瀏覽代碼

Add menus

Miguel de Icaza 7 年之前
父節點
當前提交
60823d06dd
共有 5 個文件被更改,包括 267 次插入59 次删除
  1. 19 0
      Core.cs
  2. 37 8
      Driver.cs
  3. 28 0
      README.md
  4. 172 41
      Views/Menu.cs
  5. 11 10
      demo.cs

+ 19 - 0
Core.cs

@@ -196,6 +196,7 @@ namespace Terminal {
 			view.container = this;
 			view.container = this;
 			if (view.CanFocus)
 			if (view.CanFocus)
 				CanFocus = true;
 				CanFocus = true;
+			SetNeedsDisplay ();
 		}
 		}
 
 
 		public void Add (params View [] views)
 		public void Add (params View [] views)
@@ -233,11 +234,18 @@ namespace Terminal {
 			if (view == null)
 			if (view == null)
 				return;
 				return;
 
 
+			SetNeedsDisplay ();
+			var touched = view.Frame;
 			subviews.Remove (view);
 			subviews.Remove (view);
 			view.container = null;
 			view.container = null;
 
 
 			if (subviews.Count < 1)
 			if (subviews.Count < 1)
 				this.CanFocus = false;
 				this.CanFocus = false;
+
+			foreach (var v in subviews) {
+				if (v.Frame.IntersectsWith (touched))
+					view.SetNeedsDisplay ();
+			}
 		}
 		}
 
 
 		/// <summary>
 		/// <summary>
@@ -375,6 +383,17 @@ namespace Terminal {
 		/// <value>The focused.</value>
 		/// <value>The focused.</value>
 		public View Focused => 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;
+			}
+		}
+
 		/// <summary>
 		/// <summary>
 		/// Displays the specified character in the specified column and row.
 		/// Displays the specified character in the specified column and row.
 		/// </summary>
 		/// </summary>

+ 37 - 8
Driver.cs

@@ -27,6 +27,14 @@ namespace Terminal {
 		White
 		White
 	}
 	}
 
 
+	/// <summary>
+	/// Attributes are used as elements that contain both a foreground and a background or platform specific features
+	/// </summary>
+	/// <remarks>
+	///   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.
+	/// </remarks>
 	public struct Attribute {
 	public struct Attribute {
 		internal int value;
 		internal int value;
 		public Attribute (int v)
 		public Attribute (int v)
@@ -38,13 +46,14 @@ namespace Terminal {
 		public static implicit operator Attribute (int v) => new Attribute (v);
 		public static implicit operator Attribute (int v) => new Attribute (v);
 	}
 	}
 
 
+	/// <summary>
+	/// Color scheme definitions
+	/// </summary>
 	public class ColorScheme {
 	public class ColorScheme {
 		public Attribute Normal;
 		public Attribute Normal;
 		public Attribute Focus;
 		public Attribute Focus;
 		public Attribute HotNormal;
 		public Attribute HotNormal;
 		public Attribute HotFocus;
 		public Attribute HotFocus;
-		public Attribute Marked => HotNormal;
-		public Attribute MarkedSelected => HotFocus;
 	}
 	}
 
 
 	public static class Colors {
 	public static class Colors {
@@ -230,15 +239,28 @@ namespace Terminal {
 				return;
 				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) {
 			if (wch == 27) {
 				Curses.timeout (100);
 				Curses.timeout (100);
 
 
 				code = Curses.get_wch (out wch);
 				code = Curses.get_wch (out wch);
 				if (code == Curses.KEY_CODE_YES)
 				if (code == Curses.KEY_CODE_YES)
 					handler.ProcessKey (new KeyEvent (Key.AltMask | MapCursesKey (wch)));
 					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
 			} else
 				handler.ProcessKey (new KeyEvent ((Key)wch));
 				handler.ProcessKey (new KeyEvent ((Key)wch));
 		}
 		}
@@ -310,10 +332,17 @@ namespace Terminal {
 				Colors.Base.Focus = MakeColor (Curses.COLOR_BLACK, Curses.COLOR_CYAN);
 				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.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.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.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.Normal = MakeColor (Curses.COLOR_BLACK, Curses.COLOR_WHITE);
 				Colors.Dialog.Focus = MakeColor (Curses.COLOR_BLACK, Curses.COLOR_CYAN);
 				Colors.Dialog.Focus = MakeColor (Curses.COLOR_BLACK, Curses.COLOR_CYAN);
 				Colors.Dialog.HotNormal = MakeColor (Curses.COLOR_BLUE, Curses.COLOR_WHITE);
 				Colors.Dialog.HotNormal = MakeColor (Curses.COLOR_BLUE, Curses.COLOR_WHITE);

+ 28 - 0
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.

+ 172 - 41
Views/Menu.cs

@@ -1,4 +1,13 @@
-using System;
+//
+// Authors:
+//   Miguel de Icaza ([email protected])
+//
+// TODO:
+//   Add accelerator support (ShortCut in MenuItem)
+//   Add mouse support
+//   Allow menus inside menus
+
+using System;
 namespace Terminal {
 namespace Terminal {
 
 
 	/// <summary>
 	/// <summary>
@@ -10,12 +19,31 @@ namespace Terminal {
 			Title = title ?? "";
 			Title = title ?? "";
 			Help = help ?? "";
 			Help = help ?? "";
 			Action = action;
 			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 Title { get; set; }
 		public string Help { get; set; }
 		public string Help { get; set; }
 		public Action Action { get; set; }
 		public Action Action { get; set; }
-		public int Width { get; set; }
+		internal int Width => Title.Length + Help.Length + 1 + 2;
 	}
 	}
 
 
 	/// <summary>
 	/// <summary>
@@ -33,6 +61,93 @@ namespace Terminal {
 		public int Current { get; set; }
 		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;
+		}
+	}
+
 	/// <summary>
 	/// <summary>
 	/// A menu bar for your application.
 	/// A menu bar for your application.
 	/// </summary>
 	/// </summary>
@@ -40,6 +155,7 @@ namespace Terminal {
 		public MenuBarItem [] Menus { get; set; }
 		public MenuBarItem [] Menus { get; set; }
 		int selected;
 		int selected;
 		Action action;
 		Action action;
+		bool opened;
 
 
 		public MenuBar (MenuBarItem [] menus) : base (new Rect (0, 0, Application.Driver.Cols, 1))
 		public MenuBar (MenuBarItem [] menus) : base (new Rect (0, 0, Application.Driver.Cols, 1))
 		{
 		{
@@ -87,30 +203,7 @@ namespace Terminal {
 			}
 			}
 			max += 4;
 			max += 4;
 			DrawFrame (new Rect (col, line, max, menu.Children.Length + 2), true);
 			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)
 		public override void Redraw (Rect region)
@@ -121,26 +214,24 @@ namespace Terminal {
 				Driver.AddCh (' ');
 				Driver.AddCh (' ');
 
 
 			Move (1, 0);
 			Move (1, 0);
-			int pos = 0;
+			int pos = 1;
+
 			for (int i = 0; i < Menus.Length; i++) {
 			for (int i = 0; i < Menus.Length; i++) {
 				var menu = Menus [i];
 				var menu = Menus [i];
 				if (i == selected) {
 				if (i == selected) {
 					DrawMenu (i, pos, 1);
 					DrawMenu (i, pos, 1);
-					Driver.SetAttribute (Colors.Menu.MarkedSelected);
-				} else
-					Driver.SetAttribute (Colors.Menu.Focus);
-
+				}
 				Move (pos, 0);
 				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 ();
 			PositionCursor ();
 		}
 		}
@@ -166,6 +257,46 @@ namespace Terminal {
 			action = item.Action;
 			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)
 		public override bool ProcessKey (KeyEvent kb)
 		{
 		{
 			switch (kb.Key) {
 			switch (kb.Key) {

+ 11 - 10
demo.cs

@@ -19,7 +19,8 @@ class Demo {
 			new TextField (14, 4, 40, "") { Secret = true },
 			new TextField (14, 4, 40, "") { Secret = true },
 			new CheckBox (3, 6, "Remember me"),
 			new CheckBox (3, 6, "Remember me"),
 			new Button (3, 8, "Ok"),
 			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 win = new Window (new Rect (0, 1, tframe.Width, tframe.Height-1), "Hello");
 		var menu = new MenuBar (new MenuBarItem [] {
 		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)
 			})
 			})
 		});
 		});