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