Browse Source

Add menu, ugly looking for now

Miguel de Icaza 7 years ago
parent
commit
cf2ea67e2b
8 changed files with 435 additions and 14 deletions
  1. 19 0
      Core.cs
  2. 14 0
      Driver.cs
  3. 1 0
      Event.cs
  4. 7 11
      TODO.md
  5. 2 0
      Terminal.csproj
  6. 126 0
      Views/Checkbox.cs
  7. 245 0
      Views/Menu.cs
  8. 21 3
      demo.cs

+ 19 - 0
Core.cs

@@ -317,6 +317,25 @@ namespace Terminal {
 			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>
 		/// This moves the cursor to the specified column and row in the view.
 		/// </summary>

+ 14 - 0
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

+ 1 - 0
Event.cs

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

+ 7 - 11
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
+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.
+# Merge Responder into View
+
+For now it is split, in case we want to introduce formal view controllers.  But the design becomes very ugly.
 
 # Bugs
 
-On the demo, press tab twice, instead of selecting Ok, the first tab
-does nothing, the second tab clears the screen.
+# Mouse support
+
+It is still pending.
 
-	=> 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
 
-# Make HasFocus implicitly call SetNeedsDisplay

+ 2 - 0
Terminal.csproj

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

+ 126 - 0
Views/Checkbox.cs

@@ -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 - 0
Views/Menu.cs

@@ -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;
+		}
+	}
+
+}

+ 21 - 3
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 ();
 	}
 }