Jelajahi Sumber

Merge branch 'develop' into fixes_2205_fakeclipboard

Tig 2 tahun lalu
induk
melakukan
e8f5d55523

+ 4 - 1
Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs

@@ -1401,7 +1401,10 @@ namespace Terminal.Gui {
 		/// <param name="buffer"></param>
 		public static void Write (char [] buffer)
 		{
-			throw new NotImplementedException ();
+			_buffer [CursorLeft, CursorTop] = (char)0;
+			foreach (var ch in buffer) {
+				_buffer [CursorLeft, CursorTop] += ch;
+			}
 		}
 
 		//

+ 8 - 3
Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs

@@ -132,12 +132,12 @@ namespace Terminal.Gui {
 					needMove = false;
 				}
 				if (runeWidth < 2 && ccol > 0
-					&& Rune.ColumnWidth ((char)contents [crow, ccol - 1, 0]) > 1) {
+					&& Rune.ColumnWidth ((Rune)contents [crow, ccol - 1, 0]) > 1) {
 
 					contents [crow, ccol - 1, 0] = (int)(uint)' ';
 
 				} else if (runeWidth < 2 && ccol <= Clip.Right - 1
-					&& Rune.ColumnWidth ((char)contents [crow, ccol, 0]) > 1) {
+					&& Rune.ColumnWidth ((Rune)contents [crow, ccol, 0]) > 1) {
 
 					contents [crow, ccol + 1, 0] = (int)(uint)' ';
 					contents [crow, ccol + 1, 2] = 1;
@@ -262,7 +262,12 @@ namespace Terminal.Gui {
 						if (color != redrawColor)
 							SetColor (color);
 
-						FakeConsole.Write ((char)contents [row, col, 0]);
+						Rune rune = contents [row, col, 0];
+						if (Rune.DecodeSurrogatePair (rune, out char [] spair)) {
+							FakeConsole.Write (spair);
+						} else {
+							FakeConsole.Write ((char)rune);
+						}
 						contents [row, col, 2] = 0;
 					}
 				}

+ 3 - 4
Terminal.Gui/Core/ConsoleDriver.cs

@@ -681,7 +681,7 @@ namespace Terminal.Gui {
 		/// <param name="col">Column to move the cursor to.</param>
 		/// <param name="row">Row to move the cursor to.</param>
 		public abstract void Move (int col, int row);
-		
+
 		/// <summary>
 		/// Adds the specified rune to the display at the current cursor position.
 		/// </summary>
@@ -696,11 +696,10 @@ namespace Terminal.Gui {
 		/// <returns></returns>
 		public static Rune MakePrintable (Rune c)
 		{
-			var controlChars = c & 0xFFFF;
-			if (controlChars <= 0x1F || controlChars >= 0X7F && controlChars <= 0x9F) {
+			if (c <= 0x1F || (c >= 0X7F && c <= 0x9F)) {
 				// ASCII (C0) control characters.
 				// C1 control characters (https://www.aivosto.com/articles/control-characters.html#c1)
-				return new Rune (controlChars + 0x2400);
+				return new Rune (c + 0x2400);
 			}
 
 			return c;

+ 8 - 14
Terminal.Gui/Core/TextFormatter.cs

@@ -293,12 +293,6 @@ namespace Terminal.Gui {
 			}
 		}
 
-		/// <summary>
-		/// Specifies the mask to apply to the hotkey to tag it as the hotkey. The default value of <c>0x100000</c> causes
-		/// the underlying Rune to be identified as a "private use" Unicode character.
-		/// </summary>HotKeyTagMask
-		public uint HotKeyTagMask { get; set; } = 0x100000;
-
 		/// <summary>
 		/// Gets the cursor position from <see cref="HotKey"/>. If the <see cref="HotKey"/> is defined, the cursor will be positioned over it.
 		/// </summary>
@@ -317,8 +311,9 @@ namespace Terminal.Gui {
 			get {
 				// With this check, we protect against subclasses with overrides of Text
 				if (ustring.IsNullOrEmpty (Text) || Size.IsEmpty) {
-					lines = new List<ustring> ();
-					lines.Add (ustring.Empty);
+					lines = new List<ustring> {
+						ustring.Empty
+					};
 					NeedsFormat = false;
 					return lines;
 				}
@@ -716,7 +711,7 @@ namespace Terminal.Gui {
 		}
 
 		static char [] whitespace = new char [] { ' ', '\t' };
-		private int hotKeyPos;
+		private int hotKeyPos = -1;
 
 		/// <summary>
 		/// Reformats text into lines, applying text alignment and optionally wrapping text to new lines on word boundaries.
@@ -1113,14 +1108,13 @@ namespace Terminal.Gui {
 		/// <returns>The text with the hotkey tagged.</returns>
 		/// <remarks>
 		/// The returned string will not render correctly without first un-doing the tag. To undo the tag, search for 
-		/// Runes with a bitmask of <c>otKeyTagMask</c> and remove that bitmask.
 		/// </remarks>
 		public ustring ReplaceHotKeyWithTag (ustring text, int hotPos)
 		{
 			// Set the high bit
 			var runes = text.ToRuneList ();
 			if (Rune.IsLetterOrNumber (runes [hotPos])) {
-				runes [hotPos] = new Rune ((uint)runes [hotPos] | HotKeyTagMask);
+				runes [hotPos] = new Rune ((uint)runes [hotPos]);
 			}
 			return ustring.Make (runes);
 		}
@@ -1297,13 +1291,13 @@ namespace Terminal.Gui {
 							rune = runes [idx];
 						}
 					}
-					if ((rune & HotKeyTagMask) == HotKeyTagMask) {
+					if (idx == HotKeyPos) {
 						if ((isVertical && textVerticalAlignment == VerticalTextAlignment.Justified) ||
-						    (!isVertical && textAlignment == TextAlignment.Justified)) {
+						(!isVertical && textAlignment == TextAlignment.Justified)) {
 							CursorPosition = idx - start;
 						}
 						Application.Driver?.SetAttribute (hotColor);
-						Application.Driver?.AddRune ((Rune)((uint)rune & ~HotKeyTagMask));
+						Application.Driver?.AddRune (rune);
 						Application.Driver?.SetAttribute (normalColor);
 					} else {
 						Application.Driver?.AddRune (rune);

+ 2 - 2
Terminal.Gui/Core/View.cs

@@ -987,13 +987,13 @@ namespace Terminal.Gui {
 			if (view == null || subviews == null)
 				return;
 
-			SetNeedsLayout ();
-			SetNeedsDisplay ();
 			var touched = view.Frame;
 			subviews.Remove (view);
 			tabIndexes.Remove (view);
 			view.container = null;
 			view.tabIndex = -1;
+			SetNeedsLayout ();
+			SetNeedsDisplay ();
 			if (subviews.Count < 1) {
 				CanFocus = false;
 			}

+ 3 - 4
Terminal.Gui/Core/Window.cs

@@ -277,18 +277,17 @@ namespace Terminal.Gui {
 		{
 			var padding = Border.GetSumThickness ();
 			var scrRect = ViewToScreen (new Rect (0, 0, Frame.Width, Frame.Height));
-			//var borderLength = Border.DrawMarginFrame ? 1 : 0;
 
-			// FIXED: Why do we draw the frame twice? This call is here to clear the content area, I think. Why not just clear that area?
-			if (!NeedDisplay.IsEmpty) {
+			if (!NeedDisplay.IsEmpty || ChildNeedsDisplay || LayoutNeeded) {
 				Driver.SetAttribute (GetNormalColor ());
 				Clear ();
+				contentView.SetNeedsDisplay ();
 			}
 			var savedClip = contentView.ClipToBounds ();
 
 			// Redraw our contentView
 			// DONE: smartly constrict contentView.Bounds to just be what intersects with the 'bounds' we were passed
-			contentView.Redraw (!NeedDisplay.IsEmpty ? contentView.Bounds : bounds);
+			contentView.Redraw (!NeedDisplay.IsEmpty || ChildNeedsDisplay || LayoutNeeded ? contentView.Bounds : bounds);
 			Driver.Clip = savedClip;
 
 			ClearLayoutNeeded ();

+ 1 - 1
Terminal.Gui/Terminal.Gui.csproj

@@ -16,7 +16,7 @@
   </PropertyGroup>
   <ItemGroup>
     <PackageReference Include="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.3" PrivateAssets="all" />
-    <PackageReference Include="NStack.Core" Version="1.*" />
+    <PackageReference Include="NStack.Core" Version="1.0.7" />
     <InternalsVisibleTo Include="UnitTests" />
   </ItemGroup>
   <!-- Uncomment the RestoreSources element to have dotnet restore pull NStack from a local dir for testing -->

+ 17 - 1
Terminal.Gui/Views/Menu.cs

@@ -690,6 +690,7 @@ namespace Terminal.Gui {
 				}
 			} while (barItems.Children [current] == null || disabled);
 			SetNeedsDisplay ();
+			SetParentSetNeedsDisplay ();
 			if (!host.UseSubMenusSingleFrame)
 				host.OnMenuOpened ();
 			return true;
@@ -737,11 +738,24 @@ namespace Terminal.Gui {
 				}
 			} while (barItems.Children [current] == null || disabled);
 			SetNeedsDisplay ();
+			SetParentSetNeedsDisplay ();
 			if (!host.UseSubMenusSingleFrame)
 				host.OnMenuOpened ();
 			return true;
 		}
 
+		private void SetParentSetNeedsDisplay ()
+		{
+			if (host.openSubMenu != null) {
+				foreach (var menu in host.openSubMenu) {
+					menu.SetNeedsDisplay ();
+				}
+			}
+
+			host?.openMenu.SetNeedsDisplay ();
+			host.SetNeedsDisplay ();
+		}
+
 		public override bool MouseEvent (MouseEvent me)
 		{
 			if (!host.handled && !host.HandleGrabView (me, this)) {
@@ -778,6 +792,7 @@ namespace Terminal.Gui {
 					current = me.Y - 1;
 				if (host.UseSubMenusSingleFrame || !CheckSubMenu ()) {
 					SetNeedsDisplay ();
+					SetParentSetNeedsDisplay ();
 					return true;
 				}
 				host.OnMenuOpened ();
@@ -806,6 +821,7 @@ namespace Terminal.Gui {
 				return host.CloseMenu (false, true);
 			} else {
 				SetNeedsDisplay ();
+				SetParentSetNeedsDisplay ();
 			}
 			return true;
 		}
@@ -1589,7 +1605,7 @@ namespace Terminal.Gui {
 					var subMenu = openCurrentMenu.current > -1 && openCurrentMenu.barItems.Children.Length > 0
 						? openCurrentMenu.barItems.SubMenu (openCurrentMenu.barItems.Children [openCurrentMenu.current])
 						: null;
-					if ((selectedSub == -1 || openSubMenu == null || openSubMenu?.Count == selectedSub) && subMenu == null) {
+					if ((selectedSub == -1 || openSubMenu == null || openSubMenu?.Count - 1 == selectedSub) && subMenu == null) {
 						if (openSubMenu != null && !CloseMenu (false, true))
 							return;
 						NextMenu (false, ignoreUseSubMenusSingleFrame);

+ 1 - 1
UnitTests/ConsoleDriverTests.cs

@@ -639,7 +639,7 @@ namespace Terminal.Gui.ConsoleDrivers {
 		[InlineData (0x0000001F, 0x241F)]
 		[InlineData (0x0000007F, 0x247F)]
 		[InlineData (0x0000009F, 0x249F)]
-		[InlineData (0x0001001A, 0x241A)]
+		[InlineData (0x0001001A, 0x1001A)]
 		public void MakePrintable_Converts_Control_Chars_To_Proper_Unicode (uint code, uint expected)
 		{
 			var actual = ConsoleDriver.MakePrintable (code);

+ 80 - 0
UnitTests/MenuTests.cs

@@ -1654,5 +1654,85 @@ Edit
 			Assert.True (menu.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
 			Assert.True (menu.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
 		}
+
+		[Fact, AutoInitShutdown]
+		public void MenuBar_In_Window_Without_Other_Views ()
+		{
+			var win = new Window ();
+			var menu = new MenuBar (new MenuBarItem [] {
+				new MenuBarItem ("File", new MenuItem [] {
+					new MenuItem ("New", "", null)
+				}),
+				new MenuBarItem ("Edit", new MenuItem [] {
+					new MenuBarItem ("Delete", new MenuItem [] {
+						new MenuItem ("All", "", null),
+						new MenuItem ("Selected", "", null)
+					})
+				})
+			}); ;
+			win.Add (menu);
+			var top = Application.Top;
+			top.Add (win);
+			Application.Begin (top);
+			((FakeDriver)Application.Driver).SetBufferSize (40, 8);
+
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+┌──────────────────────────────────────┐
+│ File  Edit                           │
+│                                      │
+│                                      │
+│                                      │
+│                                      │
+│                                      │
+└──────────────────────────────────────┘", output);
+
+			Assert.True (win.ProcessHotKey (new KeyEvent (Key.F9, new KeyModifiers ())));
+			win.Redraw (win.Bounds);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+┌──────────────────────────────────────┐
+│ File  Edit                           │
+│┌──────┐                              │
+││ New  │                              │
+│└──────┘                              │
+│                                      │
+│                                      │
+└──────────────────────────────────────┘", output);
+
+			Assert.True (menu.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+			win.Redraw (win.Bounds);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+┌──────────────────────────────────────┐
+│ File  Edit                           │
+│      ┌─────────┐                     │
+│      │ Delete ►│                     │
+│      └─────────┘                     │
+│                                      │
+│                                      │
+└──────────────────────────────────────┘", output);
+
+			Assert.True (menu.openMenu.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+			win.Redraw (win.Bounds);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+┌──────────────────────────────────────┐
+│ File  Edit                           │
+│      ┌─────────┐                     │
+│      │ Delete ►│┌───────────┐        │
+│      └─────────┘│ All       │        │
+│                 │ Selected  │        │
+│                 └───────────┘        │
+└──────────────────────────────────────┘", output);
+
+			Assert.True (menu.openMenu.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ())));
+			win.Redraw (win.Bounds);
+			TestHelpers.AssertDriverContentsWithFrameAre (@"
+┌──────────────────────────────────────┐
+│ File  Edit                           │
+│┌──────┐                              │
+││ New  │                              │
+│└──────┘                              │
+│                                      │
+│                                      │
+└──────────────────────────────────────┘", output);
+		}
 	}
 }

+ 16 - 8
UnitTests/TestHelpers.cs

@@ -92,7 +92,15 @@ class TestHelpers {
 
 		for (int r = 0; r < driver.Rows; r++) {
 			for (int c = 0; c < driver.Cols; c++) {
-				sb.Append ((char)contents [r, c, 0]);
+				Rune rune = contents [r, c, 0];
+				if (Rune.DecodeSurrogatePair (rune, out char [] spair)) {
+					sb.Append (spair);
+				} else {
+					sb.Append ((char)rune);
+				}
+				if (Rune.ColumnWidth (rune) > 1) {
+					c++;
+				}
 			}
 			sb.AppendLine ();
 		}
@@ -121,7 +129,7 @@ class TestHelpers {
 
 	public static Rect AssertDriverContentsWithFrameAre (string expectedLook, ITestOutputHelper output)
 	{
-		var lines = new List<List<char>> ();
+		var lines = new List<List<Rune>> ();
 		var sb = new StringBuilder ();
 		var driver = ((FakeDriver)Application.Driver);
 		var x = -1;
@@ -132,15 +140,15 @@ class TestHelpers {
 		var contents = driver.Contents;
 
 		for (int r = 0; r < driver.Rows; r++) {
-			var runes = new List<char> ();
+			var runes = new List<Rune> ();
 			for (int c = 0; c < driver.Cols; c++) {
-				var rune = (char)contents [r, c, 0];
+				var rune = (Rune)contents [r, c, 0];
 				if (rune != ' ') {
 					if (x == -1) {
 						x = c;
 						y = r;
 						for (int i = 0; i < c; i++) {
-							runes.InsertRange (i, new List<char> () { ' ' });
+							runes.InsertRange (i, new List<Rune> () { ' ' });
 						}
 					}
 					if (Rune.ColumnWidth (rune) > 1) {
@@ -169,7 +177,7 @@ class TestHelpers {
 
 		// Remove trailing whitespace on each line
 		for (int r = 0; r < lines.Count; r++) {
-			List<char> row = lines [r];
+			List<Rune> row = lines [r];
 			for (int c = row.Count - 1; c >= 0; c--) {
 				var rune = row [c];
 				if (rune != ' ' || (row.Sum (x => Rune.ColumnWidth (x)) == w)) {
@@ -179,9 +187,9 @@ class TestHelpers {
 			}
 		}
 
-		// Convert char list to string
+		// Convert Rune list to string
 		for (int r = 0; r < lines.Count; r++) {
-			var line = new string (lines [r].ToArray ());
+			var line = NStack.ustring.Make (lines [r]).ToString ();
 			if (r == lines.Count - 1) {
 				sb.Append (line);
 			} else {

+ 104 - 8
UnitTests/TextFormatterTests.cs

@@ -2424,38 +2424,38 @@ namespace Terminal.Gui.Core {
 			var tf = new TextFormatter ();
 			ustring text = "test";
 			int hotPos = 0;
-			uint tag = tf.HotKeyTagMask | 't';
+			uint tag = 't';
 
 			Assert.Equal (ustring.Make (new Rune [] { tag, 'e', 's', 't' }), tf.ReplaceHotKeyWithTag (text, hotPos));
 
-			tag = tf.HotKeyTagMask | 'e';
+			tag = 'e';
 			hotPos = 1;
 			Assert.Equal (ustring.Make (new Rune [] { 't', tag, 's', 't' }), tf.ReplaceHotKeyWithTag (text, hotPos));
 
 			var result = tf.ReplaceHotKeyWithTag (text, hotPos);
-			Assert.Equal ('e', (uint)(result.ToRunes () [1] & ~tf.HotKeyTagMask));
+			Assert.Equal ('e', (uint)(result.ToRunes () [1]));
 
 			text = "Ok";
-			tag = 0x100000 | 'O';
+			tag = 'O';
 			hotPos = 0;
 			Assert.Equal (ustring.Make (new Rune [] { tag, 'k' }), result = tf.ReplaceHotKeyWithTag (text, hotPos));
-			Assert.Equal ('O', (uint)(result.ToRunes () [0] & ~tf.HotKeyTagMask));
+			Assert.Equal ('O', (uint)(result.ToRunes () [0]));
 
 			text = "[◦ Ok ◦]";
 			text = ustring.Make (new Rune [] { '[', '◦', ' ', 'O', 'k', ' ', '◦', ']' });
 			var runes = text.ToRuneList ();
 			Assert.Equal (text.RuneCount, runes.Count);
 			Assert.Equal (text, ustring.Make (runes));
-			tag = tf.HotKeyTagMask | 'O';
+			tag = 'O';
 			hotPos = 3;
 			Assert.Equal (ustring.Make (new Rune [] { '[', '◦', ' ', tag, 'k', ' ', '◦', ']' }), result = tf.ReplaceHotKeyWithTag (text, hotPos));
-			Assert.Equal ('O', (uint)(result.ToRunes () [3] & ~tf.HotKeyTagMask));
+			Assert.Equal ('O', (uint)(result.ToRunes () [3]));
 
 			text = "^k";
 			tag = '^';
 			hotPos = 0;
 			Assert.Equal (ustring.Make (new Rune [] { tag, 'k' }), result = tf.ReplaceHotKeyWithTag (text, hotPos));
-			Assert.Equal ('^', (uint)(result.ToRunes () [0] & ~tf.HotKeyTagMask));
+			Assert.Equal ('^', (uint)(result.ToRunes () [0]));
 		}
 
 		[Fact]
@@ -4163,5 +4163,101 @@ This TextFormatter (tf2) is rewritten.
 			Assert.Equal ("你", ((Rune)usToRunes [9]).ToString ());
 			Assert.Equal ("你", s [9].ToString ());
 		}
+
+		[Fact, AutoInitShutdown]
+		public void Non_Bmp_ConsoleWidth_ColumnWidth_Equal_Two ()
+		{
+			ustring us = "\U0001d539";
+			Rune r = 0x1d539;
+
+			Assert.Equal ("𝔹", us);
+			Assert.Equal ("𝔹", r.ToString ());
+			Assert.Equal (us, r.ToString ());
+
+			Assert.Equal (2, us.ConsoleWidth);
+			Assert.Equal (2, Rune.ColumnWidth (r));
+
+			var win = new Window (us);
+			var label = new Label (ustring.Make (r));
+			var tf = new TextField (us) { Y = 1, Width = 3 };
+			win.Add (label, tf);
+			var top = Application.Top;
+			top.Add (win);
+
+			Application.Begin (top);
+			((FakeDriver)Application.Driver).SetBufferSize (10, 4);
+
+			var expected = @"
+┌ 𝔹 ────┐
+│𝔹      │
+│𝔹      │
+└────────┘";
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+
+			TestHelpers.AssertDriverContentsAre (expected, output);
+
+			var expectedColors = new Attribute [] {
+				// 0
+				Colors.Base.Normal,
+				// 1
+				Colors.Base.Focus,
+				// 2
+				Colors.Base.HotNormal
+			};
+
+			TestHelpers.AssertDriverColorsAre (@"
+0222200000
+0000000000
+0111000000
+0000000000", expectedColors);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void CJK_Compatibility_Ideographs_ConsoleWidth_ColumnWidth_Equal_Two ()
+		{
+			ustring us = "\U0000f900";
+			Rune r = 0xf900;
+
+			Assert.Equal ("豈", us);
+			Assert.Equal ("豈", r.ToString ());
+			Assert.Equal (us, r.ToString ());
+
+			Assert.Equal (2, us.ConsoleWidth);
+			Assert.Equal (2, Rune.ColumnWidth (r));
+
+			var win = new Window (us);
+			var label = new Label (ustring.Make (r));
+			var tf = new TextField (us) { Y = 1, Width = 3 };
+			win.Add (label, tf);
+			var top = Application.Top;
+			top.Add (win);
+
+			Application.Begin (top);
+			((FakeDriver)Application.Driver).SetBufferSize (10, 4);
+
+			var expected = @"
+┌ 豈 ────┐
+│豈      │
+│豈      │
+└────────┘";
+			TestHelpers.AssertDriverContentsWithFrameAre (expected, output);
+
+			TestHelpers.AssertDriverContentsAre (expected, output);
+
+			var expectedColors = new Attribute [] {
+				// 0
+				Colors.Base.Normal,
+				// 1
+				Colors.Base.Focus,
+				// 2
+				Colors.Base.HotNormal
+			};
+
+			TestHelpers.AssertDriverColorsAre (@"
+0222200000
+0000000000
+0111000000
+0000000000", expectedColors);
+		}
 	}
 }