From 35a247556af5964fceeca73dfb51651b91ed566f Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 16 Nov 2019 21:57:00 +0000 Subject: [PATCH 01/23] Added sub menus into menu bar with mouse and key navigation --- Example/demo.cs | 176 +++- Terminal.Gui/Core.cs | 51 +- Terminal.Gui/Drivers/ConsoleDriver.cs | 4 + Terminal.Gui/Views/Menu.cs | 1304 +++++++++++++++---------- 4 files changed, 981 insertions(+), 554 deletions(-) diff --git a/Example/demo.cs b/Example/demo.cs index 34a78ec2e..347ee57cb 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)) { @@ -143,7 +155,9 @@ static class Demo { new TimeField (3, 20, DateTime.Now), new TimeField (23, 20, DateTime.Now, true), progress, - new Label (3, 22, "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 ); @@ -164,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 ();}), }), @@ -177,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); @@ -211,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 @@ -223,7 +238,7 @@ static class Demo { var d = new OpenDialog ("Open", "Open a file"); Application.Run (d); - 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) @@ -254,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 () @@ -295,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") { @@ -312,27 +386,63 @@ 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 ()), + 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; @@ -341,14 +451,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 32bb3f630..a74aed5a2 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; @@ -430,7 +432,7 @@ namespace Terminal.Gui { SetNeedsDisplay (Bounds); } - bool layoutNeeded = true; + internal bool layoutNeeded = true; internal void SetNeedsLayout () { @@ -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; } @@ -1283,6 +1290,19 @@ namespace Terminal.Gui { /// toplevel and then invoke with the /// new toplevel. /// + /// + /// TopLevels can also opt-in to more sophisticated initialization + /// by implementing . When they do + /// so, the and + /// methods will be called + /// before running the view. + /// If first-run-only initialization is preferred, the + /// can be implemented too, in which case the + /// methods will only be called if + /// is . This allows proper View inheritance hierarchies + /// to override base class layout code optimally by doing so only on first run, + /// instead of on every run. + /// /// public class Toplevel : View { /// @@ -1799,25 +1819,25 @@ namespace Terminal.Gui { { var chain = toplevels.ToList(); foreach (var topLevel in chain) { - if (topLevel.Modal) - break; if (topLevel.ProcessHotKey (ke)) return; + if (topLevel.Modal) + break; } foreach (var topLevel in chain) { - if (topLevel.Modal) - break; if (topLevel.ProcessKey (ke)) return; + if (topLevel.Modal) + break; } foreach (var topLevel in chain) { - if (topLevel.Modal) - break; // Process the key normally if (topLevel.ProcessColdKey (ke)) return; + if (topLevel.Modal) + break; } } @@ -2041,11 +2061,28 @@ namespace Terminal.Gui { DrawBounds (state.Toplevel); state.Toplevel.PositionCursor (); Driver.Refresh (); + } else if (CheckLayoutNeeded (state.Toplevel)) { + TerminalResized (); + layoutNeeded = false; } else Driver.UpdateCursor (); } } + static bool layoutNeeded; + static bool CheckLayoutNeeded (View view) + { + if (view.layoutNeeded) + return layoutNeeded = view.layoutNeeded; + + for (int i = 0; view.Subviews.Count > i; i++) { + CheckLayoutNeeded (view.Subviews [i]); + if (layoutNeeded) + return layoutNeeded; + } + return layoutNeeded; + } + internal static bool DebugDrawBounds; // Need to look into why this does not work properly. diff --git a/Terminal.Gui/Drivers/ConsoleDriver.cs b/Terminal.Gui/Drivers/ConsoleDriver.cs index 7ab4139f5..88682b90e 100644 --- a/Terminal.Gui/Drivers/ConsoleDriver.cs +++ b/Terminal.Gui/Drivers/ConsoleDriver.cs @@ -142,6 +142,10 @@ namespace Terminal.Gui { /// public Attribute Normal; /// + /// The default color for text, when the view is disabled. + /// + public Attribute Disabled; + /// /// The color for text when the view has the focus. /// public Attribute Focus; diff --git a/Terminal.Gui/Views/Menu.cs b/Terminal.Gui/Views/Menu.cs index 31e28b088..9b2e8c6ce 100644 --- a/Terminal.Gui/Views/Menu.cs +++ b/Terminal.Gui/Views/Menu.cs @@ -1,520 +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 = 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; - } - } - - /// - /// 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; + } + } + +} From 956bb82f0cce307704e8d399479b30bd0824f791 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 16 Nov 2019 23:23:02 +0000 Subject: [PATCH 02/23] Needed to assume color for the Disable attribute --- Terminal.Gui/Drivers/NetDriver.cs | 1 + 1 file changed, 1 insertion(+) 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); From b5bd6964566d87d9b03b68979ada65f4010beb1a Mon Sep 17 00:00:00 2001 From: BDisp Date: Sun, 17 Nov 2019 00:04:10 +0000 Subject: [PATCH 03/23] Added Colors.Menu.Disabled to CursesDriver.cs --- Terminal.Gui/Drivers/CursesDriver.cs | 1 + 1 file changed, 1 insertion(+) 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); From f8e9df097a4e4d7008093bb054c4fff41d93c925 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sun, 17 Nov 2019 21:22:41 +0000 Subject: [PATCH 04/23] Adding a disabled menu item in the demo file --- Example/demo.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Example/demo.cs b/Example/demo.cs index 347ee57cb..b7bf1e79e 100644 --- a/Example/demo.cs +++ b/Example/demo.cs @@ -409,6 +409,7 @@ static class Demo { 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; }) }), From c7fec78c23168b638356a152cc9c3ec0598cc4e4 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sun, 17 Nov 2019 21:40:19 +0000 Subject: [PATCH 05/23] Adding a disabled menu item in the demo file --- Terminal.Gui/Drivers/WindowsDriver.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Terminal.Gui/Drivers/WindowsDriver.cs b/Terminal.Gui/Drivers/WindowsDriver.cs index 6ad870c92..6368bef19 100644 --- a/Terminal.Gui/Drivers/WindowsDriver.cs +++ b/Terminal.Gui/Drivers/WindowsDriver.cs @@ -759,6 +759,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); From 834d18484951273153b947fc9e3b0f7a6fcdbea4 Mon Sep 17 00:00:00 2001 From: Marius Ungureanu Date: Sun, 3 Nov 2019 04:20:08 +0200 Subject: [PATCH 06/23] Switch netcoreapp target to netstandard2.0 (#284) Fixes #283 Instead of adding another target framework to the list, switch from netcoreapp2.0 to netstandard2.0, as ns2.0 is a subset of netcoreapp. --- Terminal.Gui/Terminal.Gui.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj index 18afec86f..108351e96 100644 --- a/Terminal.Gui/Terminal.Gui.csproj +++ b/Terminal.Gui/Terminal.Gui.csproj @@ -1,6 +1,6 @@ - net461;netcoreapp2.0 + net461;netstandard2.0 Terminal.Gui Terminal.Gui bin\Release\Terminal.Gui.xml From 1fe2186790713ca215a6ad18c95e3a8a938acf2d Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino Date: Sat, 2 Nov 2019 23:22:55 -0300 Subject: [PATCH 07/23] Added TextView.TextChanged event (#264) --- Terminal.Gui/Views/TextView.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index 243fd29c4..f0f42b5cc 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -1,4 +1,4 @@ -// +// // TextView.cs: multi-line text editing // // Authors: @@ -266,6 +266,8 @@ namespace Terminal.Gui { bool selecting; //bool used; + public event EventHandler TextChanged; + #if false /// /// Changed event, raised when the text has clicked. @@ -312,6 +314,7 @@ namespace Terminal.Gui { set { ResetPosition (); model.LoadString (value); + TextChanged?.Invoke(this, new EventArgs()); SetNeedsDisplay (); } } From 5d00f8ac8360e48dd97af7c1cd51b324145c454c Mon Sep 17 00:00:00 2001 From: miguel Date: Mon, 4 Nov 2019 10:00:32 -0500 Subject: [PATCH 08/23] Prepare for 0.25 --- Terminal.Gui/Terminal.Gui.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj index 108351e96..bbf65b172 100644 --- a/Terminal.Gui/Terminal.Gui.csproj +++ b/Terminal.Gui/Terminal.Gui.csproj @@ -18,7 +18,7 @@ Miguel de Icaza Application framework for creating modern console applications using .NET Gui.cs is a framework for creating console user interfaces - 0.24: the Windows driver implements WakeUp, allowing some scenarios like bug #207 to be fixed; + 0.25: Added new TimeField from Jörg Preiß; Fixes for Backtab by Martin Björkström; ListView now supports simple selection; Bug fixes by giladlevi, Daniel Cazzulino and Marius Ungureanu; New Application.Run of T entry point by Daniel Cazzulino; Added various View methods to bring forward, backwards and move views in the hierarchy; Switch to Portable PDBs by Daniel Cazzulino; Dims can now be compared by Daniel Cazzulino; OnMenuOpen handler by giladlevi; Various memory usage optimizations by giladlevi; FileDialog.FilePath is now a full path by Yanwei Wang; ISupportInitialize/ISupportInitializeNotification is now supported thanks to the work from Daniel Cazzulino; Support for non-modal TopLevels by Daniel Cazzulino and Adrian Alonso; 0.24: the Windows driver implements WakeUp, allowing some scenarios like bug #207 to be fixed; 0.23: Better support for disabled menu items; Raises text changed event after the internals have been updated; Fix Caps-NumLock; Alt-HotKey now work on menus 0.22: Correct vertical scrollview behavior, Small curses driver fix for terminals without mouse support, TextView support for scrolling, Surface Used property on TextField, Surface Cursor on RadioGroup. From a6c794328fdf2e51956ec4c380436741f24453ec Mon Sep 17 00:00:00 2001 From: Miguel de Icaza Date: Tue, 5 Nov 2019 12:36:15 -0500 Subject: [PATCH 09/23] Remove travis link --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index dfbff07bf..6f46f764a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -6,7 +6,7 @@ variables: steps: - script: | - msbuild /t:Restore $TRAVIS_BUILD_DIR/Terminal.sln + msbuild /t:Restore Terminal.sln displayName: Restore dependencies - script: | From f4648560fbb0e0013cb0643b9c711f2f962fb7f2 Mon Sep 17 00:00:00 2001 From: miguel Date: Tue, 26 Nov 2019 20:46:04 -0500 Subject: [PATCH 10/23] Revert Daniel's change 00c5997daaa40f0ee73c8b7d7eccf7b4dfd04194 as it prevents the solution from building on Mac --- .travis.yml | 2 +- Designer/Designer.csproj | 55 +++++++++++++++++++++++++++----- Example/Example.csproj | 51 +++++++++++++++++++++++++---- README.md | 12 ------- Terminal.Gui/Terminal.Gui.csproj | 10 +++++- 5 files changed, 101 insertions(+), 29 deletions(-) diff --git a/.travis.yml b/.travis.yml index 32a026fbe..37ca3f8f7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,6 @@ script: - msbuild /t:Restore $TRAVIS_BUILD_DIR/Terminal.sln - cd $TRAVIS_BUILD_DIR/ - msbuild /p:Configuration=Release Terminal.sln -mono: "5.20.0" +mono: "5.20.1" dotnet: "2.1.4" os: "osx" diff --git a/Designer/Designer.csproj b/Designer/Designer.csproj index 72cddf4bc..a81a74d95 100644 --- a/Designer/Designer.csproj +++ b/Designer/Designer.csproj @@ -1,17 +1,56 @@ - + + - net472 + Debug + x86 + {1228D992-C801-49BB-839A-7BD28A3FFF0A} Exe - Terminal - Terminal + Designer + Designer + v4.6.1 + + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 true - AnyCPU;x86 - x86 + x86 + + + true + bin\Release + prompt + 4 + true + x86 - + + + ..\packages\NStack.Core.0.11.0\lib\netstandard1.5\NStack.dll + + + ..\packages\NStack.Core.0.11.0\lib\netstandard1.5\NStack.dll + - + + + + + {00F366F8-DEE4-482C-B9FD-6DB0200B79E5} + Terminal.Gui + + + + + + + \ No newline at end of file diff --git a/Example/Example.csproj b/Example/Example.csproj index f1e193d5a..3f6523667 100644 --- a/Example/Example.csproj +++ b/Example/Example.csproj @@ -1,18 +1,55 @@ - + + - net472 + Debug + x86 + {B0A602CD-E176-449D-8663-64238D54F857} Exe Terminal Terminal + v4.6.1 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 true - AnyCPU;x86 - x86 - StandaloneExample\**\*.*;Terminal.Gui\**\*.* + x86 + + + true + bin\Release + prompt + 4 + bin\Release\Terminal.xml + true + x86 - + + + ..\packages\NStack.Core.0.11.0\lib\netstandard1.5\NStack.dll + False + + + ..\packages\NStack.Core.0.11.0\lib\netstandard1.5\NStack.dll + - + + + + {00F366F8-DEE4-482C-B9FD-6DB0200B79E5} + Terminal.Gui + + + + + + diff --git a/README.md b/README.md index b9c2edba8..caa46939f 100644 --- a/README.md +++ b/README.md @@ -213,15 +213,3 @@ package version has been updated on the Terminal.Gui/Terminal.Gui.csproj, and push. Then once the package is built, VSTS will request an approval. - - -# Known Issues - -There is a [known issue](https://github.com/NuGet/Home/issues/4837) that causes the -referenced package assemblies to be missing from the output directory of your console -apps and therefore fail at run-time. In order to force the copying, you can add the -following property to your console project: - -```xml - true -``` diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj index bbf65b172..677657304 100644 --- a/Terminal.Gui/Terminal.Gui.csproj +++ b/Terminal.Gui/Terminal.Gui.csproj @@ -9,7 +9,7 @@ true Terminal.Gui - 0.25 + 0.24 Miguel de Icaza MIT https://github.com/migueldeicaza/gui.cs/ @@ -69,6 +69,14 @@ + + + ..\..\..\Users\miguel\.nuget\packages\nstack.core\0.11.0\lib\netstandard1.5\NStack.dll + + + ..\..\..\Users\miguel\.nuget\packages\nstack.core\0.11.0\lib\netstandard1.5\NStack.dll + + From e3636bd1f2f375beb13738750366f64e21051268 Mon Sep 17 00:00:00 2001 From: Miguel de Icaza Date: Sat, 29 Feb 2020 10:51:36 -0500 Subject: [PATCH 11/23] Prepare for 0.26 --- Terminal.Gui/Terminal.Gui.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj index 677657304..8dee18aa1 100644 --- a/Terminal.Gui/Terminal.Gui.csproj +++ b/Terminal.Gui/Terminal.Gui.csproj @@ -9,7 +9,7 @@ true Terminal.Gui - 0.24 + 0.65 Miguel de Icaza MIT https://github.com/migueldeicaza/gui.cs/ From 5588c7bfd4344bf0e13e2b8aa8b401ca3d1fd5f1 Mon Sep 17 00:00:00 2001 From: Miguel de Icaza Date: Sat, 29 Feb 2020 11:11:48 -0500 Subject: [PATCH 12/23] Restore some files that were deleted by Daniel's commit that I had not restored --- Designer/Designer.csproj | 3 +-- Designer/packages.config | 4 ++++ Example/packages.config | 4 ++++ Terminal.Gui/packages.config | 5 +++++ packages.config | 4 ++++ 5 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 Designer/packages.config create mode 100644 Example/packages.config create mode 100644 Terminal.Gui/packages.config create mode 100644 packages.config diff --git a/Designer/Designer.csproj b/Designer/Designer.csproj index a81a74d95..e87bd0a6b 100644 --- a/Designer/Designer.csproj +++ b/Designer/Designer.csproj @@ -40,7 +40,6 @@ - @@ -53,4 +52,4 @@ - \ No newline at end of file + diff --git a/Designer/packages.config b/Designer/packages.config new file mode 100644 index 000000000..e18eee05a --- /dev/null +++ b/Designer/packages.config @@ -0,0 +1,4 @@ + + + + diff --git a/Example/packages.config b/Example/packages.config new file mode 100644 index 000000000..e18eee05a --- /dev/null +++ b/Example/packages.config @@ -0,0 +1,4 @@ + + + + diff --git a/Terminal.Gui/packages.config b/Terminal.Gui/packages.config new file mode 100644 index 000000000..7c57b8167 --- /dev/null +++ b/Terminal.Gui/packages.config @@ -0,0 +1,5 @@ + + + + + diff --git a/packages.config b/packages.config new file mode 100644 index 000000000..e18eee05a --- /dev/null +++ b/packages.config @@ -0,0 +1,4 @@ + + + + From 8e58d05270a62f21d08b3998617a9b01ad47d279 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 29 Feb 2020 16:44:16 +0000 Subject: [PATCH 13/23] Fixed out of range exception and text redraw when navigate backward (#320) --- Terminal.Gui/Views/TextView.cs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index f0f42b5cc..8d239e86c 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -807,7 +807,7 @@ namespace Terminal.Gui { currentRow--; if (currentRow < topRow) { topRow--; - + SetNeedsDisplay (); } currentLine = GetCurrentLine (); currentColumn = currentLine.Count; @@ -1149,18 +1149,19 @@ namespace Terminal.Gui { SuperView.SetFocus (this); - var maxCursorPositionableLine = (model.Count - 1) - topRow; - if (ev.Y > maxCursorPositionableLine) { - currentRow = maxCursorPositionableLine; - } else { - currentRow = ev.Y + topRow; + if (model.Count > 0) { + var maxCursorPositionableLine = (model.Count - 1) - topRow; + if (ev.Y > maxCursorPositionableLine) { + currentRow = maxCursorPositionableLine; + } else { + currentRow = ev.Y + topRow; + } + var r = GetCurrentLine (); + if (ev.X - leftColumn >= r.Count) + currentColumn = r.Count - leftColumn; + else + currentColumn = ev.X - leftColumn; } - var r = GetCurrentLine (); - if (ev.X - leftColumn >= r.Count) - currentColumn = r.Count - leftColumn; - else - currentColumn = ev.X - leftColumn; - PositionCursor (); return true; } From 2b3d6c985d41d1249e07e945884e5d13dd1050d0 Mon Sep 17 00:00:00 2001 From: imaras Date: Sat, 29 Feb 2020 17:44:46 +0100 Subject: [PATCH 14/23] Typo fix (#321) --- docfx/articles/overview.md | 2 +- docs/articles/overview.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docfx/articles/overview.md b/docfx/articles/overview.md index aee9be278..bbabe2efa 100644 --- a/docfx/articles/overview.md +++ b/docfx/articles/overview.md @@ -128,7 +128,7 @@ void SetupMyView (View myView) Y = 2, Width = 30, Height = 1 - } + }; myView.Add (username); } ``` diff --git a/docs/articles/overview.html b/docs/articles/overview.html index df75bf79f..af659aba6 100644 --- a/docs/articles/overview.html +++ b/docs/articles/overview.html @@ -165,7 +165,7 @@ the Subviews. You can add a view to an existing view, by calling the Y = 2, Width = 30, Height = 1 - } + }; myView.Add (username); }

The container of a given view is called the SuperView and it is a property of every From 0188b954cce377675c71e17076d63c91fee27104 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 29 Feb 2020 16:45:29 +0000 Subject: [PATCH 15/23] Fixes issue #306 async/await hang (#312) * Fixed async/await hang * Fixed async/await hang with calling Wakeup * Moved Wake Up into lock statement --- Terminal.Gui/Core.cs | 2 +- Terminal.Gui/MonoCurses/mainloop.cs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Terminal.Gui/Core.cs b/Terminal.Gui/Core.cs index a74aed5a2..6ec68a5df 100644 --- a/Terminal.Gui/Core.cs +++ b/Terminal.Gui/Core.cs @@ -1724,7 +1724,7 @@ namespace Terminal.Gui { public override void Post (SendOrPostCallback d, object state) { - mainLoop.AddIdle (() => { + mainLoop.AddIdle (() => { d (state); return false; }); diff --git a/Terminal.Gui/MonoCurses/mainloop.cs b/Terminal.Gui/MonoCurses/mainloop.cs index 98ba8aca1..280c4dfee 100644 --- a/Terminal.Gui/MonoCurses/mainloop.cs +++ b/Terminal.Gui/MonoCurses/mainloop.cs @@ -354,8 +354,10 @@ namespace Mono.Terminal { ///

public Func AddIdle (Func idleHandler) { - lock (idleHandlers) + lock (idleHandlers) { idleHandlers.Add (idleHandler); + driver.Wakeup (); + } return idleHandler; } From 64724d18a92f77eef128e44a98126f800f0e647b Mon Sep 17 00:00:00 2001 From: Fabian R Date: Sat, 29 Feb 2020 10:47:29 -0600 Subject: [PATCH 16/23] Fixed and Enabled Library reinitialization (#291) - Replaced static driver initialization with property getter for reference passing in Core.cs::View class, this allows the library to be reinitialized at any time. - Made the Shutdown method on Core.cs::Application class public, since there is no reason to keep it private. Applications can shutdown the library and revert the console to the initial stage by calling it. - Fixed a memory-leak on Drivers/WindowsDriver class by destroying the generated screen buffers at library shutdown by calling CloseHandle. - Minor change to Core.cs::Application.Init(Func) for better initialization status tracking, via backend property instead of relying on the Top field. --- Terminal.Gui/Core.cs | 11 +++++++---- Terminal.Gui/Drivers/WindowsDriver.cs | 8 ++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/Terminal.Gui/Core.cs b/Terminal.Gui/Core.cs index 6ec68a5df..a6c2b16f1 100644 --- a/Terminal.Gui/Core.cs +++ b/Terminal.Gui/Core.cs @@ -245,7 +245,7 @@ namespace Terminal.Gui { /// Points to the current driver in use by the view, it is a convenience property /// for simplifying the development of new views. /// - public static ConsoleDriver Driver = Application.Driver; + public static ConsoleDriver Driver { get { return Application.Driver; } } static IList empty = new List (0).AsReadOnly (); @@ -1748,13 +1748,15 @@ namespace Terminal.Gui { /// public static void Init () => Init (() => Toplevel.Create ()); + static bool _initialized = false; + /// /// Initializes the Application /// static void Init (Func topLevelFactory) { - if (Top != null) - return; + if (_initialized) return; + _initialized = true; var p = Environment.OSVersion.Platform; Mono.Terminal.IMainLoopDriver mainLoopDriver; @@ -1985,9 +1987,10 @@ namespace Terminal.Gui { runState.Dispose (); } - static void Shutdown () + public static void Shutdown () { Driver.End (); + _initialized = false; } static void Redraw (View view) diff --git a/Terminal.Gui/Drivers/WindowsDriver.cs b/Terminal.Gui/Drivers/WindowsDriver.cs index 6368bef19..49f33f99e 100644 --- a/Terminal.Gui/Drivers/WindowsDriver.cs +++ b/Terminal.Gui/Drivers/WindowsDriver.cs @@ -100,6 +100,11 @@ namespace Terminal.Gui { var err = Marshal.GetLastWin32Error (); Console.WriteLine ("Error: {0}", err); } + + if (ScreenBuffer != IntPtr.Zero) + CloseHandle(ScreenBuffer); + + ScreenBuffer = IntPtr.Zero; } private bool ContinueListeningForConsoleEvents = true; @@ -352,6 +357,9 @@ namespace Terminal.Gui { [DllImport ("kernel32.dll", SetLastError = true)] static extern IntPtr GetStdHandle (int nStdHandle); + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool CloseHandle(IntPtr handle); + [DllImport ("kernel32.dll", EntryPoint = "ReadConsoleInputW", CharSet = CharSet.Unicode)] public static extern bool ReadConsoleInput ( IntPtr hConsoleInput, From 9ddfda04be4c406c656256cda98e372c7d42a5f9 Mon Sep 17 00:00:00 2001 From: Timothy Date: Sun, 1 Mar 2020 00:48:46 +0800 Subject: [PATCH 17/23] Moved `ListView.ListWrapper` out of `ListView` migueldeicaza/gui.cs#313` (#315) --- Terminal.Gui/Views/ListView.cs | 124 ++++++++++++++++----------------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 729a898c5..ec3450ff0 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -95,68 +95,6 @@ namespace Terminal.Gui { int top; int selected; - // - // This class is the built-in IListDataSource that renders arbitrary - // IList instances - // - class ListWrapper : IListDataSource { - IList src; - BitArray marks; - int count; - - public ListWrapper (IList source) - { - count = source.Count; - marks = new BitArray (count); - this.src = source; - } - - public int Count => src.Count; - - void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width) - { - int byteLen = ustr.Length; - int used = 0; - for (int i = 0; i < byteLen;) { - (var rune, var size) = Utf8.DecodeRune (ustr, i, i - byteLen); - var count = Rune.ColumnWidth (rune); - if (used+count >= width) - break; - driver.AddRune (rune); - used += count; - i += size; - } - for (; used < width; used++) { - driver.AddRune (' '); - } - } - - public void Render (ListView container, ConsoleDriver driver, bool marked, int item, int col, int line, int width) - { - container.Move (col, line); - var t = src [item]; - if (t is ustring) { - RenderUstr (driver, (ustring)t, col, line, width); - } else if (t is string) { - RenderUstr (driver, (string)t, col, line, width); - } else - RenderUstr (driver, t.ToString (), col, line, width); - } - - public bool IsMarked (int item) - { - if (item >= 0 && item < count) - return marks [item]; - return false; - } - - public void SetMark (int item, bool value) - { - if (item >= 0 && item < count) - marks [item] = value; - } - } - IListDataSource source; /// /// Gets or sets the IListDataSource backing this view, use SetSource() if you want to set a new IList source. @@ -437,4 +375,66 @@ namespace Terminal.Gui { return true; } } + + /// + /// This class is the built-in IListDataSource that renders arbitrary + /// IList instances + /// + public class ListWrapper : IListDataSource { + IList src; + BitArray marks; + int count; + + public ListWrapper (IList source) + { + count = source.Count; + marks = new BitArray (count); + this.src = source; + } + + public int Count => src.Count; + + void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width) + { + int byteLen = ustr.Length; + int used = 0; + for (int i = 0; i < byteLen;) { + (var rune, var size) = Utf8.DecodeRune (ustr, i, i - byteLen); + var count = Rune.ColumnWidth (rune); + if (used+count >= width) + break; + driver.AddRune (rune); + used += count; + i += size; + } + for (; used < width; used++) { + driver.AddRune (' '); + } + } + + public void Render (ListView container, ConsoleDriver driver, bool marked, int item, int col, int line, int width) + { + container.Move (col, line); + var t = src [item]; + if (t is ustring) { + RenderUstr (driver, (ustring)t, col, line, width); + } else if (t is string) { + RenderUstr (driver, (string)t, col, line, width); + } else + RenderUstr (driver, t.ToString (), col, line, width); + } + + public bool IsMarked (int item) + { + if (item >= 0 && item < count) + return marks [item]; + return false; + } + + public void SetMark (int item, bool value) + { + if (item >= 0 && item < count) + marks [item] = value; + } + } } From fe043047dbb192fa58b022efcfc7b69807f907a9 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 29 Feb 2020 16:50:37 +0000 Subject: [PATCH 18/23] Resizing the MessageBox width to accommodate all message text (#299) * Fixed key events traversal for modal dialogs The key must be propagated initially to the first chain element before evaluating if we should stop because of the handled or the Modal condition. Otherwise the modal dialog won't get any process key notification. This fixes https://github.com/migueldeicaza/gui.cs/commit/c072e29a684068af50e1b9e284213b3839dad804 * Resizing the MessageBox width to accommodate all message text Co-authored-by: Adrian Alonso --- Terminal.Gui/Dialogs/MessageBox.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Terminal.Gui/Dialogs/MessageBox.cs b/Terminal.Gui/Dialogs/MessageBox.cs index e038bf757..bbb53acf9 100644 --- a/Terminal.Gui/Dialogs/MessageBox.cs +++ b/Terminal.Gui/Dialogs/MessageBox.cs @@ -43,7 +43,7 @@ namespace Terminal.Gui { /// Width for the window. /// Height for the window. /// Title for the query. - /// Message to display, might contain multiple lines.. + /// Message to display, might contain multiple lines. /// Array of buttons to add. public static int ErrorQuery (int width, int height, string title, string message, params string [] buttons) { @@ -55,10 +55,10 @@ namespace Terminal.Gui { int textWidth = Label.MaxWidth (message, width); int clicked = -1, count = 0; - var d = new Dialog (title, width, height); + var d = new Dialog (title, Math.Max(width, textWidth) + 4, height); if (useErrorColors) d.ColorScheme = Colors.Error; - + foreach (var s in buttons) { int n = count++; var b = new Button (s); @@ -69,7 +69,7 @@ namespace Terminal.Gui { d.AddButton (b); } if (message != null) { - var l = new Label ((width - 4 - textWidth) / 2, 0, message); + var l = new Label (textWidth > width ? 0 : (width - 4 - textWidth) / 2, 0, message); d.Add (l); } From 5f11d0bdffed14a8d6968f202498ba657620a8d2 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 29 Feb 2020 16:51:15 +0000 Subject: [PATCH 19/23] Timefield format with bounds values (#303) * Implemented lower and upper bounds to TimeField * Passing old text to the Changed event handler * Change sepChar from char to string in TimeField * Changing comparison from ':' to sepChar.ToCharArray () [0] --- Terminal.Gui/Views/TextField.cs | 7 ++-- Terminal.Gui/Views/TimeField.cs | 68 +++++++++++++++++++++++++++------ 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index 215c66064..16187a339 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -35,7 +35,7 @@ namespace Terminal.Gui { /// Client code can hook up to this event, it is /// raised when the text in the entry changes. /// - public event EventHandler Changed; + public event EventHandler Changed; /// /// Public constructor that creates a text field, with layout controlled with X, Y, Width and Height. @@ -98,7 +98,10 @@ namespace Terminal.Gui { } set { + ustring oldText = ustring.Make(text); text = TextModel.ToRunes (value); + Changed?.Invoke(this, oldText); + if (point > text.Count) point = Math.Max (text.Count-1, 0); @@ -193,8 +196,6 @@ namespace Terminal.Gui { void SetText (List newText) { text = newText; - if (Changed != null) - Changed (this, EventArgs.Empty); } void SetText (IEnumerable newText) diff --git a/Terminal.Gui/Views/TimeField.cs b/Terminal.Gui/Views/TimeField.cs index fc9f860d7..cacfa944c 100644 --- a/Terminal.Gui/Views/TimeField.cs +++ b/Terminal.Gui/Views/TimeField.cs @@ -1,4 +1,4 @@ -// + // // TimeField.cs: text entry for time // // Author: Jörg Preiß @@ -25,9 +25,9 @@ namespace Terminal.Gui { int longFieldLen = 8; int shortFieldLen = 5; int FieldLen { get { return isShort ? shortFieldLen : longFieldLen; } } - - string longFormat = " hh:mm:ss"; - string shortFormat = " hh:mm"; + string sepChar; + string longFormat; + string shortFormat; string Format { get { return isShort ? shortFormat : longFormat; } } @@ -40,9 +40,20 @@ namespace Terminal.Gui { /// If true, the seconds are hidden. public TimeField (int x, int y, DateTime time, bool isShort = false) : base (x, y, isShort ? 7 : 10, "") { + CultureInfo cultureInfo = CultureInfo.CurrentCulture; + sepChar = cultureInfo.DateTimeFormat.TimeSeparator; + longFormat = $" HH{sepChar}mm{sepChar}ss"; + shortFormat = $" HH{sepChar}mm"; this.isShort = isShort; CursorPosition = 1; Time = time; + Changed += TimeField_Changed; + } + + private void TimeField_Changed (object sender, ustring e) + { + if (!DateTime.TryParseExact (Text.ToString (), Format, CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result)) + Text = e; } /// @@ -72,31 +83,66 @@ namespace Terminal.Gui { bool SetText (ustring text) { - if (!DateTime.TryParseExact (text.ToString (), Format, CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result)) + ustring [] vals = text.Split (ustring.Make (sepChar)); + bool isValidTime = true; + int hour = Int32.Parse (vals [0].ToString ()); + int minute = Int32.Parse (vals [1].ToString ()); + int second = isShort ? 0 : Int32.Parse (vals [2].ToString ()); + if (hour < 0) { + isValidTime = false; + hour = 0; + vals [0] = "0"; + } else if (hour > 23) { + isValidTime = false; + hour = 23; + vals [0] = "23"; + } + if (minute < 0) { + isValidTime = false; + minute = 0; + vals [1] = "0"; + } else if (minute > 59) { + isValidTime = false; + minute = 59; + vals [1] = "59"; + } + if (second < 0) { + isValidTime = false; + second = 0; + vals [2] = "0"; + } else if (second > 59) { + isValidTime = false; + second = 59; + vals [2] = "59"; + } + string time = isShort ? $" {hour,2:00}{sepChar}{minute,2:00}" : $" {hour,2:00}{sepChar}{minute,2:00}{sepChar}{second,2:00}"; + Text = time; + + if (!DateTime.TryParseExact (text.ToString (), Format, CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result) || + !isValidTime) return false; - Text = text; return true; } void IncCursorPosition () { - if (CursorPosition == FieldLen) + if (CursorPosition == FieldLen) return; - if (Text [++CursorPosition] == ':') + if (Text [++CursorPosition] == sepChar.ToCharArray () [0]) CursorPosition++; } void DecCursorPosition () { - if (CursorPosition == 1) + if (CursorPosition == 1) return; - if (Text [--CursorPosition] == ':') + if (Text [--CursorPosition] == sepChar.ToCharArray () [0]) CursorPosition--; } void AdjCursorPosition () { - if (Text [CursorPosition] == ':') + if (Text [CursorPosition] == sepChar.ToCharArray () [0]) CursorPosition++; } From e530cfb87ae22e1a996e7712bdf4102c2290541e Mon Sep 17 00:00:00 2001 From: "Kasper B. Graversen" Date: Sat, 29 Feb 2020 17:51:49 +0100 Subject: [PATCH 20/23] extract methods on ListView to make it controlable from other controls (#310) * moveup * MoveDown * MovePageDown * MovePageUp * MarkUnmarkRow --- Terminal.Gui/Views/ListView.cs | 124 ++++++++++++++++++++------------- 1 file changed, 76 insertions(+), 48 deletions(-) diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index ec3450ff0..bdc7f75c1 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -283,69 +283,97 @@ namespace Terminal.Gui { switch (kb.Key) { case Key.CursorUp: case Key.ControlP: - if (selected > 0) { - selected--; - if (selected < top) - top = selected; - if (SelectedChanged != null) - SelectedChanged (); - SetNeedsDisplay (); - } - return true; + return MoveUp(); case Key.CursorDown: case Key.ControlN: - if (selected + 1 < source.Count) { - selected++; - if (selected >= top + Frame.Height) - top++; - if (SelectedChanged != null) - SelectedChanged (); - SetNeedsDisplay (); - } - return true; + return MoveDown(); case Key.ControlV: case Key.PageDown: - var n = (selected + Frame.Height); - if (n > source.Count) - n = source.Count - 1; - if (n != selected) { - selected = n; - if (source.Count >= Frame.Height) - top = selected; - else - top = 0; - if (SelectedChanged != null) - SelectedChanged (); - SetNeedsDisplay (); - } - return true; + return MovePageDown(); case Key.PageUp: - n = (selected - Frame.Height); - if (n < 0) - n = 0; - if (n != selected) { - selected = n; - top = selected; - if (SelectedChanged != null) - SelectedChanged (); - SetNeedsDisplay (); - } - return true; + return MovePageUp(); case Key.Space: - if (allowsMarking) { - Source.SetMark (SelectedItem, !Source.IsMarked (SelectedItem)); - SetNeedsDisplay (); + if (MarkUnmarkRow()) return true; - } - break; + else + break; } return base.ProcessKey (kb); } + public virtual bool MarkUnmarkRow(){ + if (allowsMarking){ + Source.SetMark(SelectedItem, !Source.IsMarked(SelectedItem)); + SetNeedsDisplay(); + return true; + } + + return false; + } + + public virtual bool MovePageUp(){ + int n = (selected - Frame.Height); + if (n < 0) + n = 0; + if (n != selected){ + selected = n; + top = selected; + if (SelectedChanged != null) + SelectedChanged(); + SetNeedsDisplay(); + } + + return true; + } + + public virtual bool MovePageDown(){ + var n = (selected + Frame.Height); + if (n > source.Count) + n = source.Count - 1; + if (n != selected){ + selected = n; + if (source.Count >= Frame.Height) + top = selected; + else + top = 0; + if (SelectedChanged != null) + SelectedChanged(); + SetNeedsDisplay(); + } + + return true; + } + + public virtual bool MoveDown(){ + if (selected + 1 < source.Count){ + selected++; + if (selected >= top + Frame.Height) + top++; + if (SelectedChanged != null) + SelectedChanged(); + SetNeedsDisplay(); + } + + return true; + } + + public virtual bool MoveUp(){ + if (selected > 0){ + selected--; + if (selected < top) + top = selected; + if (SelectedChanged != null) + SelectedChanged(); + SetNeedsDisplay(); + } + + return true; + } + /// /// Positions the cursor in this view /// From b92ba573640c9dcf9051d380a9cf5a8b40b96076 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 29 Feb 2020 16:54:27 +0000 Subject: [PATCH 21/23] Allowing list items selection (#302) --- Terminal.Gui/Views/ListView.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index bdc7f75c1..71ea3a709 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -151,7 +151,7 @@ namespace Terminal.Gui { set { if (source == null) return; - + if (top < 0 || top >= source.Count) throw new ArgumentException ("value"); top = value; @@ -279,7 +279,7 @@ namespace Terminal.Gui { { if (source == null) return base.ProcessKey (kb); - + switch (kb.Key) { case Key.CursorUp: case Key.ControlP: @@ -379,7 +379,10 @@ namespace Terminal.Gui { /// public override void PositionCursor() { - Move (0, selected-top); + if (allowsMarking) + Move (1, selected - top); + else + Move (0, selected - top); } public override bool MouseEvent(MouseEvent me) @@ -387,16 +390,21 @@ namespace Terminal.Gui { if (!me.Flags.HasFlag (MouseFlags.Button1Clicked)) return false; - if (!HasFocus) + if (!HasFocus) SuperView.SetFocus (this); if (source == null) return false; - + if (me.Y + top >= source.Count) return true; selected = top + me.Y; + if (allowsMarking) { + Source.SetMark (SelectedItem, !Source.IsMarked (SelectedItem)); + SetNeedsDisplay (); + return true; + } if (SelectedChanged != null) SelectedChanged(); SetNeedsDisplay (); From e5be8223c8fc47be5659a56fcba918346aca2b15 Mon Sep 17 00:00:00 2001 From: BDisp Date: Sat, 16 Nov 2019 21:57:00 +0000 Subject: [PATCH 22/23] Added sub menus into menu bar with mouse and key navigation --- Terminal.Gui/Core.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Terminal.Gui/Core.cs b/Terminal.Gui/Core.cs index a6c2b16f1..762833e0b 100644 --- a/Terminal.Gui/Core.cs +++ b/Terminal.Gui/Core.cs @@ -2077,7 +2077,7 @@ namespace Terminal.Gui { { if (view.layoutNeeded) return layoutNeeded = view.layoutNeeded; - + for (int i = 0; view.Subviews.Count > i; i++) { CheckLayoutNeeded (view.Subviews [i]); if (layoutNeeded) From ed929f9205ca0034ae6aaf37191c45365d76daac Mon Sep 17 00:00:00 2001 From: BDisp Date: Sun, 8 Mar 2020 10:22:41 +0000 Subject: [PATCH 23/23] Fetch from upstream/master --- Designer/Designer.csproj | 2 +- Example/Example.csproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Designer/Designer.csproj b/Designer/Designer.csproj index e87bd0a6b..4374b4802 100644 --- a/Designer/Designer.csproj +++ b/Designer/Designer.csproj @@ -52,4 +52,4 @@ - + \ No newline at end of file diff --git a/Example/Example.csproj b/Example/Example.csproj index 3f6523667..95808f859 100644 --- a/Example/Example.csproj +++ b/Example/Example.csproj @@ -1,4 +1,4 @@ - + Debug @@ -52,4 +52,4 @@ - + \ No newline at end of file