From cf2ea67e2b44c15d22d2cea746a9b679e992fbfa Mon Sep 17 00:00:00 2001 From: Miguel de Icaza Date: Thu, 4 Jan 2018 22:29:48 -0500 Subject: [PATCH] Add menu, ugly looking for now --- Core.cs | 19 ++++ Driver.cs | 14 +++ Event.cs | 1 + TODO.md | 26 +++-- Terminal.csproj | 2 + Views/Checkbox.cs | 126 ++++++++++++++++++++++++ Views/Menu.cs | 245 ++++++++++++++++++++++++++++++++++++++++++++++ demo.cs | 24 ++++- 8 files changed, 439 insertions(+), 18 deletions(-) create mode 100644 Views/Checkbox.cs create mode 100644 Views/Menu.cs diff --git a/Core.cs b/Core.cs index 8c0688369..215f183e3 100644 --- a/Core.cs +++ b/Core.cs @@ -317,6 +317,25 @@ namespace Terminal { Driver.Clip = savedClip; } + /// + /// Utility function to draw strings that contain a hotkey + /// + /// String to display, the underscoore before a letter flags the next letter as the hotkey. + /// Hot color. + /// Normal color. + public void DrawHotString (string text, Attribute hotColor, Attribute normalColor) + { + Driver.SetAttribute (normalColor); + foreach (var c in text) { + if (c == '_') { + Driver.SetAttribute (hotColor); + continue; + } + Driver.AddCh (c); + Driver.SetAttribute (normalColor); + } + } + /// /// This moves the cursor to the specified column and row in the view. /// diff --git a/Driver.cs b/Driver.cs index a8e5e4201..ee750849b 100644 --- a/Driver.cs +++ b/Driver.cs @@ -52,6 +52,10 @@ namespace Terminal { } + public enum SpecialChar { + HLine, + } + public abstract class ConsoleDriver { public abstract int Cols { get; } public abstract int Rows { get; } @@ -73,6 +77,7 @@ namespace Terminal { public abstract void SetColors (short foreColorId, short backgroundColorId); public abstract void DrawFrame (Rect region, bool fill); + public abstract void AddSpecial (SpecialChar ch); Rect clip; public Rect Clip { @@ -117,6 +122,15 @@ namespace Terminal { ccol++; } + public override void AddSpecial (SpecialChar ch) + { + switch (ch) { + case SpecialChar.HLine: + AddCh (Curses.ACS_HLINE); + break; + } + } + public override void AddStr (string str) { // TODO; optimize this to determine if the str fits in the clip region, and if so, use Curses.addstr directly diff --git a/Event.cs b/Event.cs index 266d95c82..74207915a 100644 --- a/Event.cs +++ b/Event.cs @@ -44,6 +44,7 @@ namespace Terminal { ControlY, ControlZ, Esc = 27, + Enter = '\n', Space = 32, Delete = 127, diff --git a/TODO.md b/TODO.md index 746ed849e..22359a1b8 100644 --- a/TODO.md +++ b/TODO.md @@ -47,21 +47,17 @@ Unclear what to do about that right now. Needs to move to `ustring` from `NStack.Core` to get full Unicode support. -# Focus - -When SetFocus is called, it need to ensure that the chain up the views is -focused as well, something that we got for free in the old Container/Widget -model, but needs revisiting in the new model. - -# Bugs - -On the demo, press tab twice, instead of selecting Ok, the first tab -does nothing, the second tab clears the screen. - - => Explanation: the Window gets a NeedsDisplay, so it displays - tiself, but the contentView does not have NeedsDisplay - set recursively, so it does not render any of the subviews +Should get NStack.Core to move `ustring` to `System`. # Merge Responder into View -# Make HasFocus implicitly call SetNeedsDisplay +For now it is split, in case we want to introduce formal view controllers. But the design becomes very ugly. + +# Bugs + +# Mouse support + +It is still pending. + + + diff --git a/Terminal.csproj b/Terminal.csproj index de02b6d27..fea27ae6f 100644 --- a/Terminal.csproj +++ b/Terminal.csproj @@ -42,6 +42,8 @@ + + diff --git a/Views/Checkbox.cs b/Views/Checkbox.cs new file mode 100644 index 000000000..6ced2eb38 --- /dev/null +++ b/Views/Checkbox.cs @@ -0,0 +1,126 @@ +using System; + +namespace Terminal { + public class CheckBox : View { + string text; + int hot_pos = -1; + char hot_key; + + /// + /// Toggled event, raised when the CheckButton is toggled. + /// + /// + /// Client code can hook up to this event, it is + /// raised when the checkbutton is activated either with + /// the mouse or the keyboard. + /// + public event EventHandler Toggled; + + /// + /// Public constructor, creates a CheckButton based on + /// the given text at the given position. + /// + /// + /// The size of CheckButton is computed based on the + /// text length. This CheckButton is not toggled. + /// + public CheckBox (int x, int y, string s) : this (x, y, s, false) + { + } + + /// + /// Public constructor, creates a CheckButton based on + /// the given text at the given position and a state. + /// + /// + /// The size of CheckButton is computed based on the + /// text length. + /// + public CheckBox (int x, int y, string s, bool is_checked) : base (new Rect (x, y, s.Length + 4, 1)) + { + Checked = is_checked; + Text = s; + + CanFocus = true; + } + + /// + /// The state of the checkbox. + /// + public bool Checked { get; set; } + + /// + /// The text displayed by this widget. + /// + public string Text { + get { + return text; + } + + set { + text = value; + + int i = 0; + hot_pos = -1; + hot_key = (char)0; + foreach (char c in text) { + if (Char.IsUpper (c)) { + hot_key = c; + hot_pos = i; + break; + } + i++; + } + } + } + + public override void Redraw (Rect region) + { + Driver.SetAttribute (HasFocus ? Colors.Base.Focus : Colors.Base.Normal); + Move (0, 0); + Driver.AddStr (Checked ? "[x] " : "[ ] "); + Move (4, 0); + Driver.AddStr (Text); + if (hot_pos != -1) { + Move (4 + hot_pos, 0); + Driver.SetAttribute (HasFocus ? Colors.Base.HotFocus : Colors.Base.HotNormal); + Driver.AddCh (hot_key); + } + } + + public override void PositionCursor () + { + Move (1, 0); + } + + public override bool ProcessKey (KeyEvent kb) + { + if (kb.KeyValue == ' ') { + Checked = !Checked; + + if (Toggled != null) + Toggled (this, EventArgs.Empty); + + SetNeedsDisplay (); + return true; + } + return false; + } + +#if false + public override void ProcessMouse (Curses.MouseEvent ev) + { + if ((ev.ButtonState & Curses.Event.Button1Clicked) != 0){ + Container.SetFocus (this); + Container.Redraw (); + + Checked = !Checked; + + if (Toggled != null) + Toggled (this, EventArgs.Empty); + Redraw (); + } + } +#endif + } +} diff --git a/Views/Menu.cs b/Views/Menu.cs new file mode 100644 index 000000000..36da7ab7e --- /dev/null +++ b/Views/Menu.cs @@ -0,0 +1,245 @@ +using System; +namespace Terminal { + + /// + /// A menu item has a title, an associated help text, and an action to execute on activation. + /// + public class MenuItem { + public MenuItem (string title, string help, Action action) + { + Title = title ?? ""; + Help = help ?? ""; + Action = action; + Width = Title.Length + Help.Length + 1; + } + public string Title { get; set; } + public string Help { get; set; } + public Action Action { get; set; } + public int Width { get; set; } + } + + /// + /// A menu bar item contains other menu items. + /// + public class MenuBarItem { + public MenuBarItem (string title, MenuItem [] children) + { + Title = title ?? ""; + Children = children; + } + + public string Title { get; set; } + public MenuItem [] Children { get; set; } + public int Current { get; set; } + } + + /// + /// A menu bar for your application. + /// + public class MenuBar : View { + public MenuBarItem [] Menus { get; set; } + int selected; + Action action; + + public MenuBar (MenuBarItem [] menus) : base (new Rect (0, 0, Application.Driver.Cols, 1)) + { + Menus = menus; + CanFocus = false; + selected = -1; + } + + /// + /// Activates the menubar + /// + public void Activate (int idx) + { + if (idx < 0 || idx > Menus.Length) + throw new ArgumentException ("idx"); + + action = null; + selected = idx; + + foreach (var m in Menus) + m.Current = 0; + + // TODO: Application.Run (this); + selected = -1; + SuperView.SetNeedsDisplay (); + + if (action != null) + action (); + } + + void DrawMenu (int idx, int col, int line) + { + int max = 0; + var menu = Menus [idx]; + + if (menu.Children == null) + return; + + foreach (var m in menu.Children) { + if (m == null) + continue; + + if (m.Width > max) + max = m.Width; + } + 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) + { + Move (0, 0); + Driver.SetAttribute (Colors.Base.Focus); + for (int i = 0; i < Frame.Width; i++) + Driver.AddCh (' '); + + Move (1, 0); + int pos = 0; + 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; + } + 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].Title.Length + 4; + } + } + Move (0, 0); + } + + void Selected (MenuItem item) + { + // TODO: Running = false; + action = item.Action; + } + + public override bool ProcessKey (KeyEvent kb) + { + switch (kb.Key) { + case Key.CursorUp: + if (Menus [selected].Children == null) + return false; + + int current = Menus [selected].Current; + do { + current--; + if (current < 0) + current = Menus [selected].Children.Length - 1; + } while (Menus [selected].Children [current] == null); + Menus [selected].Current = current; + + SetNeedsDisplay (); + return true; + + case Key.CursorDown: + if (Menus [selected].Children == null) + return false; + + do { + Menus [selected].Current = (Menus [selected].Current + 1) % Menus [selected].Children.Length; + } while (Menus [selected].Children [Menus [selected].Current] == null); + + SetNeedsDisplay (); + break; + + case Key.CursorLeft: + selected--; + if (selected < 0) + selected = Menus.Length - 1; + break; + case Key.CursorRight: + selected = (selected + 1) % Menus.Length; + break; + + case Key.Enter: + if (Menus [selected].Children == null) + return false; + + Selected (Menus [selected].Children [Menus [selected].Current]); + 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; + } + } + +} diff --git a/demo.cs b/demo.cs index e36b73899..1ed5190b7 100644 --- a/demo.cs +++ b/demo.cs @@ -17,8 +17,9 @@ class Demo { new TextField (14, 2, 40, ""), new Label (3, 4, "Password: "), new TextField (14, 4, 40, "") { Secret = true }, - new Button (3, 6, "Ok"), - new Button (10, 6, "Cancel") + new CheckBox (3, 6, "Remember me"), + new Button (3, 8, "Ok"), + new Button (10, 8, "Cancel") ); } @@ -26,11 +27,28 @@ class Demo { { Application.Init (); var top = Application.Top; - var win = new Window (new Rect (0, 0, 80, 24), "Hello"); + var tframe = top.Frame; + + 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 ("Edit", new MenuItem [] { + new MenuItem ("Copy", "", null), + new MenuItem ("Cut", "", null), + new MenuItem ("Paste", "", null) + }) + }); ShowEntries (win); + // ShowTextAlignments (win); top.Add (win); + top.Add (menu); Application.Run (); } } \ No newline at end of file