From 60823d06dd40e668e4ebad757cf000251c555136 Mon Sep 17 00:00:00 2001 From: Miguel de Icaza Date: Fri, 5 Jan 2018 22:09:15 -0500 Subject: [PATCH] Add menus --- Core.cs | 19 +++++ Driver.cs | 45 +++++++++-- README.md | 28 +++++++ Views/Menu.cs | 213 ++++++++++++++++++++++++++++++++++++++++---------- demo.cs | 21 ++--- 5 files changed, 267 insertions(+), 59 deletions(-) create mode 100644 README.md diff --git a/Core.cs b/Core.cs index 215f183e3..fe093f5ac 100644 --- a/Core.cs +++ b/Core.cs @@ -196,6 +196,7 @@ namespace Terminal { view.container = this; if (view.CanFocus) CanFocus = true; + SetNeedsDisplay (); } public void Add (params View [] views) @@ -233,11 +234,18 @@ namespace Terminal { if (view == null) return; + SetNeedsDisplay (); + var touched = view.Frame; subviews.Remove (view); view.container = null; if (subviews.Count < 1) this.CanFocus = false; + + foreach (var v in subviews) { + if (v.Frame.IntersectsWith (touched)) + view.SetNeedsDisplay (); + } } /// @@ -375,6 +383,17 @@ namespace Terminal { /// The focused. public View Focused => focused; + public View MostFocused { + get { + if (Focused == null) + return null; + var most = Focused.MostFocused; + if (most != null) + return most; + return Focused; + } + } + /// /// Displays the specified character in the specified column and row. /// diff --git a/Driver.cs b/Driver.cs index ee750849b..a62edc77d 100644 --- a/Driver.cs +++ b/Driver.cs @@ -27,6 +27,14 @@ namespace Terminal { White } + /// + /// Attributes are used as elements that contain both a foreground and a background or platform specific features + /// + /// + /// Attributes are needed to map colors to terminal capabilities that might lack colors, on color + /// scenarios, they encode both the foreground and the background color and are used in the ColorScheme + /// class to define color schemes that can be used in your application. + /// public struct Attribute { internal int value; public Attribute (int v) @@ -38,13 +46,14 @@ namespace Terminal { public static implicit operator Attribute (int v) => new Attribute (v); } + /// + /// Color scheme definitions + /// public class ColorScheme { public Attribute Normal; public Attribute Focus; public Attribute HotNormal; public Attribute HotFocus; - public Attribute Marked => HotNormal; - public Attribute MarkedSelected => HotFocus; } public static class Colors { @@ -230,15 +239,28 @@ namespace Terminal { return; } - // Special handling for ESC, we want to try to catch ESC+letter to simulate alt-letter. + // Special handling for ESC, we want to try to catch ESC+letter to simulate alt-letter as well as Alt-Fkey if (wch == 27) { Curses.timeout (100); code = Curses.get_wch (out wch); if (code == Curses.KEY_CODE_YES) handler.ProcessKey (new KeyEvent (Key.AltMask | MapCursesKey (wch))); - if (code == 0) - handler.ProcessKey (new KeyEvent (Key.AltMask | (Key)wch)); + if (code == 0) { + KeyEvent key; + + // The ESC-number handling, debatable. + if (wch >= '1' && wch <= '9') + key = new KeyEvent ((Key)((int)Key.F1 + (wch - '0' - 1))); + else if (wch == '0') + key = new KeyEvent (Key.F10); + else if (wch == 27) + key = new KeyEvent ((Key)wch); + else + key = new KeyEvent (Key.AltMask | (Key)wch); + handler.ProcessKey (key); + } else + handler.ProcessKey (new KeyEvent (Key.Esc)); } else handler.ProcessKey (new KeyEvent ((Key)wch)); } @@ -310,10 +332,17 @@ namespace Terminal { Colors.Base.Focus = MakeColor (Curses.COLOR_BLACK, Curses.COLOR_CYAN); Colors.Base.HotNormal = Curses.A_BOLD | MakeColor (Curses.COLOR_YELLOW, Curses.COLOR_BLUE); Colors.Base.HotFocus = 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.Focus = Curses.A_BOLD | MakeColor (Curses.COLOR_YELLOW, Curses.COLOR_CYAN); - Colors.Menu.HotNormal = Curses.A_BOLD | MakeColor (Curses.COLOR_WHITE, Curses.COLOR_BLACK); + + // Focused, + // Selected, Hot: Yellow on Black + // Selected, text: white on black + // Unselected, hot: yellow on cyan + // unselected, text: same as unfocused Colors.Menu.HotFocus = Curses.A_BOLD | MakeColor (Curses.COLOR_YELLOW, Curses.COLOR_BLACK); + 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.Dialog.Normal = MakeColor (Curses.COLOR_BLACK, Curses.COLOR_WHITE); Colors.Dialog.Focus = MakeColor (Curses.COLOR_BLACK, Curses.COLOR_CYAN); Colors.Dialog.HotNormal = MakeColor (Curses.COLOR_BLUE, Curses.COLOR_WHITE); diff --git a/README.md b/README.md new file mode 100644 index 000000000..d9b7ddaf4 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# Gui.cs - Terminal UI toolkit for .NET + +This is a simple UI toolkit for .NET. + +# Input Handling + +The input handling of gui.cs is similar in some ways to Emacs and the +Midnight Commander, so you can expect some of the special key +combinations to be active. + +The key ESC can act as an Alt modifier (or Meta in Emacs parlance), to +allow input on terminals that do not have an alt key. So to produce +the sequence Alt-F, you can press either Alt-F, or ESC folowed by the key F. + +To enter the key ESC, you can either press ESC and wait 100 +milliseconds, or you can press ESC twice. + +ESC-0, and ESC_1 through ESC-9 have a special meaning, they map to +F10, and F1 to F9 respectively. + +# Driver model + +Currently gui.cs is built on top of curses, but the console driver has +been abstracted, an implementation that uses `System.Console` is +possible, but would have to emulate some of the behavior of curses, +namely that operations are performed on the buffer, and the Refresh +call reflects the contents of an internal buffer into the screen and +position the cursor in the last set position at the end. \ No newline at end of file diff --git a/Views/Menu.cs b/Views/Menu.cs index 36da7ab7e..540dd3e20 100644 --- a/Views/Menu.cs +++ b/Views/Menu.cs @@ -1,4 +1,13 @@ -using System; +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// +// TODO: +// Add accelerator support (ShortCut in MenuItem) +// Add mouse support +// Allow menus inside menus + +using System; namespace Terminal { /// @@ -10,12 +19,31 @@ namespace Terminal { Title = title ?? ""; Help = help ?? ""; Action = action; - Width = Title.Length + Help.Length + 1; + bool nextIsHot = false; + foreach (var x in title) { + if (x == '_') + nextIsHot = true; + else { + if (nextIsHot) { + HotKey = 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 char HotKey; + public Key ShortCut; + public string Title { get; set; } public string Help { get; set; } public Action Action { get; set; } - public int Width { get; set; } + internal int Width => Title.Length + Help.Length + 1 + 2; } /// @@ -33,6 +61,93 @@ namespace Terminal { public int Current { get; set; } } + class Menu : View { + MenuBarItem barItems; + MenuBar host; + + 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; + CanFocus = true; + } + + public override void Redraw (Rect region) + { + Driver.SetAttribute (Colors.Menu.Normal); + DrawFrame (region, 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 == barItems.Current ? Colors.Menu.Focus : Colors.Menu.Normal); + for (int p = 0; p < Frame.Width-2; p++) + if (item == null) + Driver.AddSpecial (SpecialChar.HLine); + else + Driver.AddCh (' '); + + if (item == null) + continue; + + Move (2, i + 1); + DrawHotString (item.Title, + i == barItems.Current ? Colors.Menu.HotFocus : Colors.Menu.HotNormal, + i == barItems.Current ? Colors.Menu.Focus : Colors.Menu.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 + barItems.Current); + } + + public override bool ProcessKey (KeyEvent kb) + { + switch (kb.Key) { + case Key.CursorUp: + barItems.Current--; + if (barItems.Current < 0) + barItems.Current = barItems.Children.Length - 1; + SetNeedsDisplay (); + break; + case Key.CursorDown: + barItems.Current++; + if (barItems.Current == barItems.Children.Length) + barItems.Current = 0; + SetNeedsDisplay (); + break; + case Key.CursorLeft: + host.PreviousMenu (); + break; + case Key.CursorRight: + host.NextMenu (); + break; + case Key.Esc: + host.CloseMenu (); + break; + } + return true; + } + } + /// /// A menu bar for your application. /// @@ -40,6 +155,7 @@ namespace Terminal { public MenuBarItem [] Menus { get; set; } int selected; Action action; + bool opened; public MenuBar (MenuBarItem [] menus) : base (new Rect (0, 0, Application.Driver.Cols, 1)) { @@ -87,30 +203,7 @@ namespace Terminal { } max += 4; DrawFrame (new Rect (col, line, max, menu.Children.Length + 2), true); - for (int i = 0; i < menu.Children.Length; i++) { - var item = menu.Children [i]; - Move (line + 1 + i, col + 1); - Driver.SetAttribute (item == null ? Colors.Base.Focus : i == menu.Current ? Colors.Menu.MarkedSelected : Colors.Menu.Marked); - for (int p = 0; p < max - 2; p++) - if (item == null) - Driver.AddSpecial (SpecialChar.HLine); - else - Driver.AddCh (' '); - - if (item == null) - continue; - - Move (line + 1 + i, col + 2); - DrawHotString (item.Title, - i == menu.Current ? Colors.Menu.HotFocus: Colors.Menu.HotNormal, - i == menu.Current ? Colors.Menu.MarkedSelected : Colors.Menu.Marked); - - // The help string - var l = item.Help.Length; - Move (col + max - l - 2, line + 1 + i); - Driver.AddStr (item.Help); - } } public override void Redraw (Rect region) @@ -121,26 +214,24 @@ namespace Terminal { Driver.AddCh (' '); Move (1, 0); - int pos = 0; + int pos = 1; + for (int i = 0; i < Menus.Length; i++) { var menu = Menus [i]; if (i == selected) { DrawMenu (i, pos, 1); - Driver.SetAttribute (Colors.Menu.MarkedSelected); - } else - Driver.SetAttribute (Colors.Menu.Focus); - + } Move (pos, 0); - Driver.AddCh (' '); - Driver.AddStr(menu.Title); - Driver.AddCh (' '); - if (HasFocus && i == selected) - Driver.SetAttribute (Colors.Menu.MarkedSelected); - else - Driver.SetAttribute (Colors.Menu.Marked); - Driver.AddStr (" "); - - pos += menu.Title.Length + 4; + Attribute hotColor, normalColor; + if (opened){ + hotColor = i == selected ? Colors.Menu.HotFocus : Colors.Menu.HotNormal; + normalColor = i == selected ? Colors.Menu.Focus : Colors.Menu.Normal; + } else { + hotColor = Colors.Base.Focus; + normalColor = Colors.Base.Focus; + } + DrawHotString (" " + menu.Title + " " + " ", hotColor, normalColor); + pos += menu.Title.Length + 3; } PositionCursor (); } @@ -166,6 +257,46 @@ namespace Terminal { action = item.Action; } + Menu openMenu; + View focusedWhenOpened; + + void OpenMenu () + { + if (openMenu != null) + return; + + focusedWhenOpened = SuperView.MostFocused; + openMenu = new Menu (this, 0, 1, Menus [0]); + // Save most deeply focused chain + SuperView.Add (openMenu); + SuperView.SetFocus (openMenu); + } + + internal void CloseMenu () + { + SetNeedsDisplay (); + SuperView.Remove (openMenu); + focusedWhenOpened.SuperView.SetFocus (focusedWhenOpened); + openMenu = null; + } + + internal void PreviousMenu () + { + } + + internal void NextMenu () + { + } + + public override bool ProcessHotKey (KeyEvent kb) + { + if (kb.Key == Key.F9) { + OpenMenu (); + return true; + } + return base.ProcessHotKey (kb); + } + public override bool ProcessKey (KeyEvent kb) { switch (kb.Key) { diff --git a/demo.cs b/demo.cs index 1ed5190b7..e5a700d1f 100644 --- a/demo.cs +++ b/demo.cs @@ -19,7 +19,8 @@ class Demo { new TextField (14, 4, 40, "") { Secret = true }, new CheckBox (3, 6, "Remember me"), new Button (3, 8, "Ok"), - new Button (10, 8, "Cancel") + new Button (10, 8, "Cancel"), + new Label (3, 18, "Press ESC and 9 to activate the menubar") ); } @@ -31,16 +32,16 @@ class Demo { var win = new Window (new Rect (0, 1, tframe.Width, tframe.Height-1), "Hello"); var menu = new MenuBar (new MenuBarItem [] { - new MenuBarItem ("File", new MenuItem [] { - new MenuItem ("New", "", null), - new MenuItem ("Open", "", null), - new MenuItem ("Close", "", null), - new MenuItem ("Quit", "", null) + new MenuBarItem ("_File", new MenuItem [] { + new MenuItem ("_New", "", null), + new MenuItem ("_Open", "", null), + new MenuItem ("_Close", "", null), + new MenuItem ("_Quit", "", null) }), - new MenuBarItem ("Edit", new MenuItem [] { - new MenuItem ("Copy", "", null), - new MenuItem ("Cut", "", null), - new MenuItem ("Paste", "", null) + new MenuBarItem ("_Edit", new MenuItem [] { + new MenuItem ("_Copy", "", null), + new MenuItem ("C_ut", "", null), + new MenuItem ("_Paste", "", null) }) });