Add menu, ugly looking for now

This commit is contained in:
Miguel de Icaza
2018-01-04 22:29:48 -05:00
parent 27a8e1ca9d
commit cf2ea67e2b
8 changed files with 439 additions and 18 deletions

19
Core.cs
View File

@@ -317,6 +317,25 @@ namespace Terminal {
Driver.Clip = savedClip; Driver.Clip = savedClip;
} }
/// <summary>
/// Utility function to draw strings that contain a hotkey
/// </summary>
/// <param name="s">String to display, the underscoore before a letter flags the next letter as the hotkey.</param>
/// <param name="hotColor">Hot color.</param>
/// <param name="normalColor">Normal color.</param>
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);
}
}
/// <summary> /// <summary>
/// This moves the cursor to the specified column and row in the view. /// This moves the cursor to the specified column and row in the view.
/// </summary> /// </summary>

View File

@@ -52,6 +52,10 @@ namespace Terminal {
} }
public enum SpecialChar {
HLine,
}
public abstract class ConsoleDriver { public abstract class ConsoleDriver {
public abstract int Cols { get; } public abstract int Cols { get; }
public abstract int Rows { get; } public abstract int Rows { get; }
@@ -73,6 +77,7 @@ namespace Terminal {
public abstract void SetColors (short foreColorId, short backgroundColorId); public abstract void SetColors (short foreColorId, short backgroundColorId);
public abstract void DrawFrame (Rect region, bool fill); public abstract void DrawFrame (Rect region, bool fill);
public abstract void AddSpecial (SpecialChar ch);
Rect clip; Rect clip;
public Rect Clip { public Rect Clip {
@@ -117,6 +122,15 @@ namespace Terminal {
ccol++; ccol++;
} }
public override void AddSpecial (SpecialChar ch)
{
switch (ch) {
case SpecialChar.HLine:
AddCh (Curses.ACS_HLINE);
break;
}
}
public override void AddStr (string str) 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 // TODO; optimize this to determine if the str fits in the clip region, and if so, use Curses.addstr directly

View File

@@ -44,6 +44,7 @@ namespace Terminal {
ControlY, ControlY,
ControlZ, ControlZ,
Esc = 27, Esc = 27,
Enter = '\n',
Space = 32, Space = 32,
Delete = 127, Delete = 127,

26
TODO.md
View File

@@ -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. Needs to move to `ustring` from `NStack.Core` to get full Unicode support.
# Focus Should get NStack.Core to move `ustring` to `System`.
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
# Merge Responder into View # 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.

View File

@@ -42,6 +42,8 @@
<Compile Include="Views\Label.cs" /> <Compile Include="Views\Label.cs" />
<Compile Include="Views\TextField.cs" /> <Compile Include="Views\TextField.cs" />
<Compile Include="Views\Button.cs" /> <Compile Include="Views\Button.cs" />
<Compile Include="Views\Checkbox.cs" />
<Compile Include="Views\Menu.cs" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Reference Include="mono-curses.dll"> <Reference Include="mono-curses.dll">

126
Views/Checkbox.cs Normal file
View File

@@ -0,0 +1,126 @@
using System;
namespace Terminal {
public class CheckBox : View {
string text;
int hot_pos = -1;
char hot_key;
/// <summary>
/// Toggled event, raised when the CheckButton is toggled.
/// </summary>
/// <remarks>
/// Client code can hook up to this event, it is
/// raised when the checkbutton is activated either with
/// the mouse or the keyboard.
/// </remarks>
public event EventHandler Toggled;
/// <summary>
/// Public constructor, creates a CheckButton based on
/// the given text at the given position.
/// </summary>
/// <remarks>
/// The size of CheckButton is computed based on the
/// text length. This CheckButton is not toggled.
/// </remarks>
public CheckBox (int x, int y, string s) : this (x, y, s, false)
{
}
/// <summary>
/// Public constructor, creates a CheckButton based on
/// the given text at the given position and a state.
/// </summary>
/// <remarks>
/// The size of CheckButton is computed based on the
/// text length.
/// </remarks>
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;
}
/// <summary>
/// The state of the checkbox.
/// </summary>
public bool Checked { get; set; }
/// <summary>
/// The text displayed by this widget.
/// </summary>
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
}
}

245
Views/Menu.cs Normal file
View File

@@ -0,0 +1,245 @@
using System;
namespace Terminal {
/// <summary>
/// A menu item has a title, an associated help text, and an action to execute on activation.
/// </summary>
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; }
}
/// <summary>
/// A menu bar item contains other menu items.
/// </summary>
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; }
}
/// <summary>
/// A menu bar for your application.
/// </summary>
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;
}
/// <summary>
/// Activates the menubar
/// </summary>
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;
}
}
}

24
demo.cs
View File

@@ -17,8 +17,9 @@ class Demo {
new TextField (14, 2, 40, ""), new TextField (14, 2, 40, ""),
new Label (3, 4, "Password: "), new Label (3, 4, "Password: "),
new TextField (14, 4, 40, "") { Secret = true }, new TextField (14, 4, 40, "") { Secret = true },
new Button (3, 6, "Ok"), new CheckBox (3, 6, "Remember me"),
new Button (10, 6, "Cancel") new Button (3, 8, "Ok"),
new Button (10, 8, "Cancel")
); );
} }
@@ -26,11 +27,28 @@ class Demo {
{ {
Application.Init (); Application.Init ();
var top = Application.Top; 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); ShowEntries (win);
// ShowTextAlignments (win); // ShowTextAlignments (win);
top.Add (win); top.Add (win);
top.Add (menu);
Application.Run (); Application.Run ();
} }
} }