From a64b4bc405f4e2935d0f29c66cc1d306df881bb5 Mon Sep 17 00:00:00 2001 From: Tig Date: Fri, 21 Mar 2025 09:17:49 +0100 Subject: [PATCH 01/28] touching publish.yml --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 545f596053..507e68e8e9 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,7 +2,7 @@ name: Publish Terminal.Gui on: push: - branches: [ v1_release, v1_develop, v2_release, v2_develop ] + branches: [ v2_release, v2_develop ] tags: - v* paths-ignore: From e089108e72992642dec6cc7037a6d637b04f743f Mon Sep 17 00:00:00 2001 From: Tonttu <15074459+TheTonttu@users.noreply.github.com> Date: Sat, 29 Mar 2025 19:18:10 +0200 Subject: [PATCH 02/28] Fixes #4000. Named colors as enums. (#4005) * Add W3C color enum with the RGB as numeric value * Add transform helper class for W3cColor enum For the sake of backwards compatibility prioritize parsing 16 color mode color names over the W3C colors because the previous resource-based color names/values had a mix of W3C and 16 color mode RGB values. Mechanism for choosing/prioritizing one color scheme over the other is currently only available at higher application/driver/output level. * IColorNameResolver enable null analysis * Remove obsolete color name related ResourceManagerTests * Replace remains of W3CColors with direct W3C color name resolver Temporarily breaks backwards compatibility and tests even further. * Add ANSI 4-bit (ColorName16) color name resolver * Add multi-standard color name resolver Combined resolver for both ANSI 4-bit (ColorName16) and W3C colors while trying to maintain backwards compatibility for ColorPicker. * Split conditional name resolver test cases * Change W3C colors tests to be similar to name resolvers * Change W3cColorsTests to W3cColorNameResolverTests More consistent when all the tests refer to the color name resolver layer. * Make W3cColors internal Color name resolver is the public interface. * W3cColors: Use Color.Argb instead of individual RGB components * MultiStandardColorNameResolver: Substitute instead of blocking alternative W3C names Changes color picker behavior a bit, e.g. Aqua will match to Cyan instead of jumping to Aquamarine. * Remove leftover color string resources * Consistent position for IColorNameResolver #nullable enable directive * Add missing XML comments to ColorScheme.Colors.cs --- .../Configuration/ColorJsonConverter.cs | 7 +- .../Drawing/Color/AnsiColorNameResolver.cs | 70 + .../Drawing/Color/Color.Formatting.cs | 148 +- Terminal.Gui/Drawing/Color/Color.cs | 11 +- .../Drawing/Color/ColorScheme.Colors.cs | 13 +- Terminal.Gui/Drawing/Color/ColorStrings.cs | 120 +- .../Drawing/Color/IColorNameResolver.cs | 10 +- .../Color/MultiStandardColorNameResolver.cs | 188 +++ Terminal.Gui/Drawing/Color/W3CColors.cs | 24 - Terminal.Gui/Drawing/Color/W3cColor.cs | 777 ++++++++++ .../Drawing/Color/W3cColorNameResolver.cs | 110 ++ Terminal.Gui/Resources/Strings.Designer.cs | 1314 ----------------- Terminal.Gui/Resources/Strings.resx | 438 ------ Terminal.Gui/Views/ColorPicker.cs | 12 +- .../Resources/ResourceManagerTests.cs | 49 - Tests/UnitTests/Views/ColorPickerTests.cs | 22 +- .../Color/AnsiColorNameResolverTests.cs | 124 ++ .../MultiStandardColorNameResolverTests.cs | 232 +++ .../Color/W3cColorNameResolverTests.cs | 150 ++ 19 files changed, 1851 insertions(+), 1968 deletions(-) create mode 100644 Terminal.Gui/Drawing/Color/AnsiColorNameResolver.cs create mode 100644 Terminal.Gui/Drawing/Color/MultiStandardColorNameResolver.cs delete mode 100644 Terminal.Gui/Drawing/Color/W3CColors.cs create mode 100644 Terminal.Gui/Drawing/Color/W3cColor.cs create mode 100644 Terminal.Gui/Drawing/Color/W3cColorNameResolver.cs create mode 100644 Tests/UnitTestsParallelizable/Drawing/Color/AnsiColorNameResolverTests.cs create mode 100644 Tests/UnitTestsParallelizable/Drawing/Color/MultiStandardColorNameResolverTests.cs create mode 100644 Tests/UnitTestsParallelizable/Drawing/Color/W3cColorNameResolverTests.cs diff --git a/Terminal.Gui/Configuration/ColorJsonConverter.cs b/Terminal.Gui/Configuration/ColorJsonConverter.cs index 6da4490d91..c490299167 100644 --- a/Terminal.Gui/Configuration/ColorJsonConverter.cs +++ b/Terminal.Gui/Configuration/ColorJsonConverter.cs @@ -1,6 +1,5 @@ using System.Text.Json; using System.Text.Json.Serialization; -using ColorHelper; namespace Terminal.Gui; @@ -40,11 +39,9 @@ public override Color Read (ref Utf8JsonReader reader, Type typeToConvert, JsonS // Get the color string ReadOnlySpan colorString = reader.GetString (); - // Check if the color string is a color name - if (ColorStrings.TryParseW3CColorName (colorString.ToString (), out Color color1)) + if (ColorStrings.TryParseNamedColor (colorString, out Color namedColor)) { - // Return the parsed color - return new (color1); + return namedColor; } if (Color.TryParse (colorString, null, out Color parsedColor)) diff --git a/Terminal.Gui/Drawing/Color/AnsiColorNameResolver.cs b/Terminal.Gui/Drawing/Color/AnsiColorNameResolver.cs new file mode 100644 index 0000000000..1b61ed0ddb --- /dev/null +++ b/Terminal.Gui/Drawing/Color/AnsiColorNameResolver.cs @@ -0,0 +1,70 @@ +#nullable enable + +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; + +namespace Terminal.Gui; + +/// +/// Color name resolver for . +/// +public class AnsiColorNameResolver : IColorNameResolver +{ + private static readonly ImmutableArray AnsiColorNames = ImmutableArray.Create(Enum.GetNames()); + + /// + public IEnumerable GetColorNames () + { + return AnsiColorNames; + } + + /// + public bool TryNameColor (Color color, [NotNullWhen (true)] out string? name) + { + if (Color.TryGetExactNamedColor16 (color, out ColorName16 colorName16)) + { + name = Color16Name (colorName16); + return true; + } + name = null; + return false; + } + + /// + public bool TryParseColor (ReadOnlySpan name, out Color color) + { + if (Enum.TryParse (name, ignoreCase: true, out ColorName16 colorName16) && + // Any numerical value converts to undefined enum value. + Enum.IsDefined (colorName16)) + { + color = new Color (colorName16); + return true; + } + color = default; + return false; + } + + private static string Color16Name (ColorName16 color16) + { + return color16 switch + { + ColorName16.Black => nameof (ColorName16.Black), + ColorName16.Blue => nameof (ColorName16.Blue), + ColorName16.Green => nameof (ColorName16.Green), + ColorName16.Cyan => nameof (ColorName16.Cyan), + ColorName16.Red => nameof (ColorName16.Red), + ColorName16.Magenta => nameof (ColorName16.Magenta), + ColorName16.Yellow => nameof (ColorName16.Yellow), + ColorName16.Gray => nameof (ColorName16.Gray), + ColorName16.DarkGray => nameof (ColorName16.DarkGray), + ColorName16.BrightBlue => nameof (ColorName16.BrightBlue), + ColorName16.BrightGreen => nameof (ColorName16.BrightGreen), + ColorName16.BrightCyan => nameof (ColorName16.BrightCyan), + ColorName16.BrightRed => nameof (ColorName16.BrightRed), + ColorName16.BrightMagenta => nameof (ColorName16.BrightMagenta), + ColorName16.BrightYellow => nameof (ColorName16.BrightYellow), + ColorName16.White => nameof (ColorName16.White), + _ => throw new NotSupportedException ($"ColorName16 '{color16}' is not supported.") + }; + } +} diff --git a/Terminal.Gui/Drawing/Color/Color.Formatting.cs b/Terminal.Gui/Drawing/Color/Color.Formatting.cs index dd775fe049..323ae14166 100644 --- a/Terminal.Gui/Drawing/Color/Color.Formatting.cs +++ b/Terminal.Gui/Drawing/Color/Color.Formatting.cs @@ -1,6 +1,5 @@ #nullable enable using System.Diagnostics.CodeAnalysis; -using System.Diagnostics.Contracts; using System.Globalization; using System.Runtime.CompilerServices; @@ -267,90 +266,79 @@ public static Color Parse (ReadOnlySpan text, IFormatProvider? formatProvi return text switch { // Null string or empty span provided - { IsEmpty: true } when formatProvider is null => throw new ColorParseException ( - in text, - "The text provided was null or empty.", - in text - ), + { IsEmpty: true } when formatProvider is null => + throw new ColorParseException (in text, "The text provided was null or empty.", in text), // A valid ICustomColorFormatter was specified and the text wasn't null or empty { IsEmpty: false } when formatProvider is ICustomColorFormatter f => f.Parse (text), // Input string is only whitespace - { Length: > 0 } when text.IsWhiteSpace () => throw new ColorParseException ( - in text, - "The text provided consisted of only whitespace characters.", - in text - ), + { Length: > 0 } when text.IsWhiteSpace () => + throw new ColorParseException (in text, "The text provided consisted of only whitespace characters.", in text), // Any string too short to possibly be any supported format. - { Length: > 0 and < 3 } => throw new ColorParseException ( - in text, - "Text was too short to be any possible supported format.", - in text - ), - - // The various hexadecimal cases - ['#', ..] hexString => hexString switch - { - // #RGB - ['#', var rChar, var gChar, var bChar] chars when chars [1..] - .IsAllAsciiHexDigits () => - new Color ( - byte.Parse ([rChar, rChar], NumberStyles.HexNumber), - byte.Parse ([gChar, gChar], NumberStyles.HexNumber), - byte.Parse ([bChar, bChar], NumberStyles.HexNumber) - ), - - // #ARGB - ['#', var aChar, var rChar, var gChar, var bChar] chars when chars [1..] - .IsAllAsciiHexDigits () => - new Color ( - byte.Parse ([rChar, rChar], NumberStyles.HexNumber), - byte.Parse ([gChar, gChar], NumberStyles.HexNumber), - byte.Parse ([bChar, bChar], NumberStyles.HexNumber), - byte.Parse ([aChar, aChar], NumberStyles.HexNumber) - ), - - // #RRGGBB - [ - '#', var r1Char, var r2Char, var g1Char, var g2Char, var b1Char, - var b2Char - ] chars when chars [1..].IsAllAsciiHexDigits () => - new Color ( - byte.Parse ([r1Char, r2Char], NumberStyles.HexNumber), - byte.Parse ([g1Char, g2Char], NumberStyles.HexNumber), - byte.Parse ([b1Char, b2Char], NumberStyles.HexNumber) - ), - - // #AARRGGBB - [ - '#', var a1Char, var a2Char, var r1Char, var r2Char, var g1Char, - var g2Char, var b1Char, var b2Char - ] chars when chars [1..].IsAllAsciiHexDigits () => - new Color ( - byte.Parse ([r1Char, r2Char], NumberStyles.HexNumber), - byte.Parse ([g1Char, g2Char], NumberStyles.HexNumber), - byte.Parse ([b1Char, b2Char], NumberStyles.HexNumber), - byte.Parse ([a1Char, a2Char], NumberStyles.HexNumber) - ), - _ => throw new ColorParseException ( - in hexString, - $"Color hex string {hexString} was not in a supported format", - in hexString - ) - }, - - // rgb(r,g,b) or rgb(r,g,b,a) - ['r', 'g', 'b', '(', .., ')'] => ParseRgbaFormat (in text, 4), - - // rgba(r,g,b,a) or rgba(r,g,b) - ['r', 'g', 'b', 'a', '(', .., ')'] => ParseRgbaFormat (in text, 5), - - // Attempt to parse as a named color from the ColorStrings resources - { } when char.IsLetter (text [0]) && ColorStrings.TryParseW3CColorName (text.ToString (), out Color color) => - new Color (color), + { Length: > 0 and < 3 } => + throw new ColorParseException (in text, "Text was too short to be any possible supported format.", in text), + // The various hexadecimal cases + ['#', ..] hexString => hexString switch + { + // #RGB + ['#', var rChar, var gChar, var bChar] chars when chars [1..] + .IsAllAsciiHexDigits () => + new Color ( + byte.Parse ([rChar, rChar], NumberStyles.HexNumber), + byte.Parse ([gChar, gChar], NumberStyles.HexNumber), + byte.Parse ([bChar, bChar], NumberStyles.HexNumber) + ), + + // #ARGB + ['#', var aChar, var rChar, var gChar, var bChar] chars when chars [1..] + .IsAllAsciiHexDigits () => + new Color ( + byte.Parse ([rChar, rChar], NumberStyles.HexNumber), + byte.Parse ([gChar, gChar], NumberStyles.HexNumber), + byte.Parse ([bChar, bChar], NumberStyles.HexNumber), + byte.Parse ([aChar, aChar], NumberStyles.HexNumber) + ), + + // #RRGGBB + [ + '#', var r1Char, var r2Char, var g1Char, var g2Char, var b1Char, var b2Char + ] chars when chars [1..].IsAllAsciiHexDigits () => + new Color ( + byte.Parse ([r1Char, r2Char], NumberStyles.HexNumber), + byte.Parse ([g1Char, g2Char], NumberStyles.HexNumber), + byte.Parse ([b1Char, b2Char], NumberStyles.HexNumber) + ), + + // #AARRGGBB + [ + '#', var a1Char, var a2Char, + var r1Char, var r2Char, + var g1Char, var g2Char, + var b1Char, var b2Char + ] chars when chars [1..].IsAllAsciiHexDigits () => + new Color ( + byte.Parse ([r1Char, r2Char], NumberStyles.HexNumber), + byte.Parse ([g1Char, g2Char], NumberStyles.HexNumber), + byte.Parse ([b1Char, b2Char], NumberStyles.HexNumber), + byte.Parse ([a1Char, a2Char], NumberStyles.HexNumber) + ), + _ => throw new ColorParseException ( + in hexString, + $"Color hex string {hexString} was not in a supported format", + in hexString + ) + }, + + // rgb(r,g,b) or rgb(r,g,b,a) + ['r', 'g', 'b', '(', .., ')'] => ParseRgbaFormat (in text, 4), + + // rgba(r,g,b,a) or rgba(r,g,b) + ['r', 'g', 'b', 'a', '(', .., ')'] => ParseRgbaFormat (in text, 5), + // Attempt named colors + { } when char.IsLetter (text [0]) && ColorStrings.TryParseNamedColor (text, out Color color) => color, // Any other input _ => throw new ColorParseException (in text, "Text did not match any expected format.", in text, []) }; @@ -585,11 +573,9 @@ public static bool TryParse (ReadOnlySpan utf8Text, IFormatProvider? provi [SkipLocalsInit] public override string ToString () { - string? name = ColorStrings.GetW3CColorName (this); - - if (name is { }) + if (ColorStrings.GetColorName (this) is string colorName) { - return name; + return colorName; } return $"#{R:X2}{G:X2}{B:X2}"; diff --git a/Terminal.Gui/Drawing/Color/Color.cs b/Terminal.Gui/Drawing/Color/Color.cs index d81254c73a..23c74c8779 100644 --- a/Terminal.Gui/Drawing/Color/Color.cs +++ b/Terminal.Gui/Drawing/Color/Color.cs @@ -1,7 +1,5 @@ #nullable enable using System.Collections.Frozen; -using System.Diagnostics.Contracts; -using System.Drawing; using System.Globalization; using System.Numerics; using System.Runtime.CompilerServices; @@ -236,6 +234,15 @@ internal static ColorName16 GetClosestNamedColor16 (Color inputColor) return ColorExtensions.ColorToName16Map.MinBy (pair => CalculateColorDistance (inputColor, pair.Key)).Value; } + /// Converts the given color value to exact named color represented by . + /// + /// Successfully converted named color. + /// True if conversion succeeded; otherwise false. + internal static bool TryGetExactNamedColor16 (Color inputColor, out ColorName16 colorName16) + { + return ColorExtensions.ColorToName16Map.TryGetValue (inputColor, out colorName16); + } + [SkipLocalsInit] private static float CalculateColorDistance (in Vector4 color1, in Vector4 color2) { return Vector4.Distance (color1, color2); } diff --git a/Terminal.Gui/Drawing/Color/ColorScheme.Colors.cs b/Terminal.Gui/Drawing/Color/ColorScheme.Colors.cs index 5c3bc6dba9..96316ac431 100644 --- a/Terminal.Gui/Drawing/Color/ColorScheme.Colors.cs +++ b/Terminal.Gui/Drawing/Color/ColorScheme.Colors.cs @@ -185,6 +185,11 @@ public bool ContainsKey (string key) } } + /// + /// Copies the elements of the to an array, starting at a particular array index. + /// + /// The one-dimensional array that is the destination of the elements copied from . + /// The zero-based index in array at which copying begins. public void CopyTo (KeyValuePair [] array, int arrayIndex) { lock (_lock) @@ -193,6 +198,10 @@ public void CopyTo (KeyValuePair [] array, int arrayIndex) } } + /// + /// Returns an enumerator that iterates through the . + /// + /// An enumerator for the . public IEnumerator> GetEnumerator () { lock (_lock) @@ -206,6 +215,7 @@ IEnumerator IEnumerable.GetEnumerator () return GetEnumerator (); } + /// public bool Remove (KeyValuePair item) { lock (_lock) @@ -219,6 +229,7 @@ public bool Remove (KeyValuePair item) } } + /// public bool Remove (string key) { lock (_lock) @@ -245,7 +256,7 @@ public bool TryGetValue (string key, out ColorScheme? value) /// /// Resets the dictionary to its default values. /// - /// + /// The reset dictionary. public static Dictionary Reset () { lock (_lock) diff --git a/Terminal.Gui/Drawing/Color/ColorStrings.cs b/Terminal.Gui/Drawing/Color/ColorStrings.cs index 79ca9f357e..fc57b7a3e1 100644 --- a/Terminal.Gui/Drawing/Color/ColorStrings.cs +++ b/Terminal.Gui/Drawing/Color/ColorStrings.cs @@ -1,8 +1,5 @@ #nullable enable -using System.Collections; using System.Globalization; -using System.Resources; -using Terminal.Gui.Resources; namespace Terminal.Gui; @@ -11,7 +8,9 @@ namespace Terminal.Gui; /// public static class ColorStrings { - // PERFORMANCE: See https://stackoverflow.com/a/15521524/297526 for why GlobalResources.GetString is fast. + private static readonly AnsiColorNameResolver Ansi = new(); + private static readonly W3cColorNameResolver W3c = new(); + private static readonly MultiStandardColorNameResolver Multi = new(); /// /// Gets the W3C standard string for . @@ -20,7 +19,39 @@ public static class ColorStrings /// if there is no standard color name for the specified color. public static string? GetW3CColorName (Color color) { - return GlobalResources.GetString ($"#{color.R:X2}{color.G:X2}{color.B:X2}", CultureInfo.CurrentUICulture); + if (W3c.TryNameColor (color, out string? name)) + { + return name; + } + return null; + } + + /// + /// Gets the ANSI 4-bit (16) color name for . + /// + /// The color. + /// if there is no standard color name for the specified color. + public static string? GetANSIColor16Name (Color color) + { + if (Ansi.TryNameColor (color, out string? name)) + { + return name; + } + return null; + } + + /// + /// Gets backwards compatible color name for . + /// + /// The color. + /// Standard color name for the specified color; otherwise . + public static string? GetColorName (Color color) + { + if (Multi.TryNameColor (color, out string? name)) + { + return name; + } + return null; } /// @@ -29,54 +60,77 @@ public static class ColorStrings /// public static IEnumerable GetW3CColorNames () { - ResourceSet? resourceSet = GlobalResources.GetResourceSet (CultureInfo.CurrentUICulture, true, true); - if (resourceSet == null) + return W3c.GetColorNames (); + } + + /// + /// Parses and returns if name is a W3C standard named color. + /// + /// The name to parse. + /// If successful, the color. + /// if was parsed successfully. + public static bool TryParseW3CColorName (ReadOnlySpan name, out Color color) + { + if (W3c.TryParseColor (name, out color)) { - yield break; + return true; } + // Backwards compatibility: Also parse #RRGGBB. + return TryParseHexColor (name, out color); + } - foreach (DictionaryEntry entry in resourceSet) + /// + /// Parses and returns if name is a ANSI 4-bit standard named color. + /// + /// The name to parse. + /// If successful, the color. + /// if was parsed successfully. + public static bool TryParseColor16 (ReadOnlySpan name, out Color color) + { + if (Ansi.TryParseColor (name, out color)) { - if (entry is { Value: string colorName, Key: string key } && key.StartsWith ('#')) - { - yield return colorName; - } + return true; } + color = default; + return false; } /// - /// Parses and returns if name is a W3C standard named color. + /// Parses and returns if name is either ANSI 4-bit or W3C standard named color. /// /// The name to parse. /// If successful, the color. /// if was parsed successfully. - public static bool TryParseW3CColorName (string name, out Color color) + public static bool TryParseNamedColor (ReadOnlySpan name, out Color color) { - foreach (DictionaryEntry entry in GlobalResources.GetResourceSet (CultureInfo.CurrentUICulture, true, true)!) + if (Multi.TryParseColor (name, out color)) { - if (entry.Value is string colorName && colorName.Equals (name, StringComparison.OrdinalIgnoreCase)) - { - return TryParseColorKey (entry.Key.ToString (), out color); - } + return true; + } + // Backwards compatibility: Also parse #RRGGBB. + if (TryParseHexColor (name, out color)) + { + return true; } - return TryParseColorKey (name, out color); + color = default; + return false; + } - bool TryParseColorKey (string? key, out Color color) + private static bool TryParseHexColor (ReadOnlySpan name, out Color color) + { + if (name.Length == 7 && name [0] == '#') { - if (key != null && key.StartsWith ('#') && key.Length == 7) + if (int.TryParse (name.Slice (1, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int r) && + int.TryParse (name.Slice (3, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int g) && + int.TryParse (name.Slice (5, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int b)) { - if (int.TryParse (key.AsSpan (1, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int r) && - int.TryParse (key.AsSpan (3, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int g) && - int.TryParse (key.AsSpan (5, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int b)) - { - color = new Color (r, g, b); - return true; - } + color = new Color (r, g, b); + return true; } - - color = default (Color); - return false; } + + color = default; + return false; } } diff --git a/Terminal.Gui/Drawing/Color/IColorNameResolver.cs b/Terminal.Gui/Drawing/Color/IColorNameResolver.cs index 5af243ee4a..cbbe70985e 100644 --- a/Terminal.Gui/Drawing/Color/IColorNameResolver.cs +++ b/Terminal.Gui/Drawing/Color/IColorNameResolver.cs @@ -1,4 +1,8 @@ -namespace Terminal.Gui; +#nullable enable + +using System.Diagnostics.CodeAnalysis; + +namespace Terminal.Gui; /// /// When implemented by a class, allows mapping to @@ -20,7 +24,7 @@ public interface IColorNameResolver /// /// /// - bool TryNameColor (Color color, out string name); + bool TryNameColor (Color color, [NotNullWhen(true)]out string? name); /// /// Returns if is a recognized @@ -30,5 +34,5 @@ public interface IColorNameResolver /// /// /// - bool TryParseColor (string name, out Color color); + bool TryParseColor (ReadOnlySpan name, out Color color); } diff --git a/Terminal.Gui/Drawing/Color/MultiStandardColorNameResolver.cs b/Terminal.Gui/Drawing/Color/MultiStandardColorNameResolver.cs new file mode 100644 index 0000000000..01554bbb31 --- /dev/null +++ b/Terminal.Gui/Drawing/Color/MultiStandardColorNameResolver.cs @@ -0,0 +1,188 @@ +#nullable enable + +using System.Collections.Frozen; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; + +namespace Terminal.Gui; + +/// +/// Backwards compatible(-ish) color name resolver prioritizing ANSI 4-bit (16) colors with fallback to W3C colors. +/// +public class MultiStandardColorNameResolver : IColorNameResolver +{ + private static readonly AnsiColorNameResolver Ansi = new(); + private static readonly W3cColorNameResolver W3c = new(); + private static readonly FrozenSet W3cBlockedColors; + private static readonly ImmutableArray CombinedColorNames; + private static readonly FrozenDictionary W3cSubstituteColors; + + static MultiStandardColorNameResolver () + { + HashSet combinedNames = new(Ansi.GetColorNames()); + + HashSet w3cInconsistentColors = new(); + Dictionary w3cSubstituteColors = new(StringComparer.OrdinalIgnoreCase); + + IEnumerable enumerableW3cNames = W3c.GetColorNames (); + IReadOnlyList w3cNames = enumerableW3cNames is IReadOnlyList alreadyReadOnlyList + ? alreadyReadOnlyList + : [.. enumerableW3cNames]; + + Dictionary> w3cColorsWithAlternativeNames = w3cNames + .GroupBy(w3cName => + { + if (!W3c.TryParseColor(w3cName, out Color w3cColor)) + { + throw new InvalidOperationException ($"W3C color name '{w3cName}' does not resolve to any W3C color."); + } + return w3cColor; + }) + .Where(g => g.Count() > 1) + .ToDictionary(g => g.Key, g => g.ToHashSet()); + + // Gather inconsistencies between ANSI and W3C, filter out or substitute problematic W3C colors and names, + // and create additional blocklist for W3C colors. + // Blocking and filtering is only applied to W3C because this resolver prioritizes ANSI for backwards compatibility. + // It would be a lot simpler to just prioritize W3C colors and names. + foreach (string w3cName in w3cNames) + { + if (w3cSubstituteColors.ContainsKey (w3cName)) + { + // Already dealt with alternative name. + continue; + } + + if (!W3c.TryParseColor (w3cName, out Color w3cColor)) + { + // This condition is just inverted to reduce indentation. + // Also it should practically never happen if the W3C color name resolver is properly implemented. + throw new InvalidOperationException ($"W3C color name '{w3cName}' does not resolve to any color."); + } + + if (w3cColorsWithAlternativeNames.TryGetValue (w3cColor, out var names)) + { + bool substituted = false; + // Alternative names cause issues with ColorPicker etc. when combined with ANSI and prioritizing ANSI resolver. + // For example Aqua is not in ColorName16 but the actual color value resolves to ANSI Cyan + // so autocomplete for Aqua suddenly changes to Cyan because they happen to have same color value in both color scheme. + // Also DarkGrey would cause inconsistencies because the alternative DarkGray exists in ANSI and has different color value. + foreach (string name in names) + { + if (Ansi.TryParseColor (name, out Color substituteColor)) + { + // Block the W3C color when it is inconsistent with the substitute color + // so there is no situation where W3C color -> color name -> ANSI color. + if (w3cColor != substituteColor) + { + w3cInconsistentColors.Add (w3cColor); + } + + // Substitute all W3C alternatives to match with the ANSI color to keep colors consistent. + foreach (string alternativeName in names) + { + w3cSubstituteColors.Add (alternativeName, substituteColor); + combinedNames.Add (alternativeName); + } + substituted = true; + break; + } + } + + if (substituted) + { + // Already dealt with, continue to next W3C color name. + continue; + } + } + + // Same name, different ANSI value. + // For example both #767676 (ColorName16) and #A9A9A9 (W3C) resolve to DarkGray, + // although a bad example because it is already substituted due to also having alternative names. + if (Ansi.TryParseColor (w3cName, out Color ansiColor) && w3cColor != ansiColor) + { + w3cInconsistentColors.Add (w3cColor); + continue; + } + + combinedNames.Add (w3cName); + } + + // TODO: Utilize .NET 9 and later alternative lookup for matching ReadOnlySpan with string. + W3cSubstituteColors = w3cSubstituteColors.ToFrozenDictionary ( + // Workaround for alternative lookup not being available in .NET 8 by matching ReadOnlySpan hash code to string hash code. + keySelector: kvp => string.GetHashCode (kvp.Key, StringComparison.OrdinalIgnoreCase), + // The string element is for detecting hash collision. + elementSelector: kvp => (kvp.Key, kvp.Value)); + W3cBlockedColors = w3cInconsistentColors.ToFrozenSet (); + CombinedColorNames = combinedNames.Order ().ToImmutableArray (); + } + + /// + public IEnumerable GetColorNames () + { + return CombinedColorNames; + } + + /// + public bool TryNameColor (Color color, [NotNullWhen (true)] out string? name) + { + if (Ansi.TryNameColor (color, out string? ansiName)) + { + name = ansiName; + return true; + } + + if (!IsBlockedW3cColor (color) && + W3c.TryNameColor (color, out string? w3cName)) + { + name = w3cName; + return true; + } + + name = null; + return false; + } + + /// + public bool TryParseColor (ReadOnlySpan name, out Color color) + { + if (Ansi.TryParseColor (name, out color)) + { + return true; + } + + if (GetSubstituteW3cColor (name, out color)) + { + return true; + } + + if (W3c.TryParseColor (name, out color) && + !IsBlockedW3cColor (color)) + { + return true; + } + + color = default; + return false; + } + + private static bool GetSubstituteW3cColor (ReadOnlySpan name, out Color substituteColor) + { + int nameHashCode = string.GetHashCode(name, StringComparison.OrdinalIgnoreCase); + if (W3cSubstituteColors.TryGetValue (nameHashCode, out var match) && + match is (string matchName, Color matchColor) && + name.Equals (matchName, StringComparison.OrdinalIgnoreCase)) + { + substituteColor = matchColor; + return true; + } + substituteColor = default; + return false; + } + + private static bool IsBlockedW3cColor (Color color) + { + return W3cBlockedColors.Contains (color); + } +} diff --git a/Terminal.Gui/Drawing/Color/W3CColors.cs b/Terminal.Gui/Drawing/Color/W3CColors.cs deleted file mode 100644 index baa1a1101e..0000000000 --- a/Terminal.Gui/Drawing/Color/W3CColors.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Terminal.Gui; - -/// -/// Helper class that resolves w3c color names to their hex values -/// Based on https://www.w3schools.com/colors/color_tryit.asp -/// -public class W3CColors : IColorNameResolver -{ - /// - public IEnumerable GetColorNames () { return ColorStrings.GetW3CColorNames (); } - - /// - public bool TryParseColor (string name, out Color color) { return ColorStrings.TryParseW3CColorName (name, out color); } - - /// - public bool TryNameColor (Color color, out string name) - { - string answer = ColorStrings.GetW3CColorName (color); - - name = answer ?? string.Empty; - - return answer != null; - } -} diff --git a/Terminal.Gui/Drawing/Color/W3cColor.cs b/Terminal.Gui/Drawing/Color/W3cColor.cs new file mode 100644 index 0000000000..a2cc07dabd --- /dev/null +++ b/Terminal.Gui/Drawing/Color/W3cColor.cs @@ -0,0 +1,777 @@ +namespace Terminal.Gui; + +/// +/// Represents the W3C color names with their RGB values. +/// +/// +/// Based on https://www.w3schools.com/colors/color_tryit.asp page. +/// +public enum W3cColor +{ + /// + /// Alice blue RGB(240, 248, 255). + /// + AliceBlue = 0xF0F8FF, + + /// + /// Antique white RGB(250, 235, 215). + /// + AntiqueWhite = 0xFAEBD7, + + /// + /// Aqua RGB(0, 255, 255). + /// + Aqua = 0x00FFFF, + + /// + /// Aquamarine RGB(127, 255, 212). + /// + Aquamarine = 0x7FFFD4, + + /// + /// Azure RGB(240, 255, 255). + /// + Azure = 0xF0FFFF, + + /// + /// Beige RGB(245, 245, 220). + /// + Beige = 0xF5F5DC, + + /// + /// Bisque RGB(255, 228, 196). + /// + Bisque = 0xFFE4C4, + + /// + /// Black RGB(0, 0, 0). + /// + Black = 0x000000, + + /// + /// Blanched almond RGB(255, 235, 205). + /// + BlanchedAlmond = 0xFFEBCD, + + /// + /// Blue RGB(0, 0, 255). + /// + Blue = 0x0000FF, + + /// + /// Blue violet RGB(138, 43, 226). + /// + BlueViolet = 0x8A2BE2, + + /// + /// Brown RGB(165, 42, 42). + /// + Brown = 0xA52A2A, + + /// + /// Burly wood RGB(222, 184, 135). + /// + BurlyWood = 0xDEB887, + + /// + /// Cadet blue RGB(95, 158, 160). + /// + CadetBlue = 0x5F9EA0, + + /// + /// Chartreuse RGB(127, 255, 0). + /// + Chartreuse = 0x7FFF00, + + /// + /// Chocolate RGB(210, 105, 30). + /// + Chocolate = 0xD2691E, + + /// + /// Coral RGB(255, 127, 80). + /// + Coral = 0xFF7F50, + + /// + /// Cornflower blue RGB(100, 149, 237). + /// + CornflowerBlue = 0x6495ED, + + /// + /// Cornsilk RGB(255, 248, 220). + /// + Cornsilk = 0xFFF8DC, + + /// + /// Crimson RGB(220, 20, 60). + /// + Crimson = 0xDC143C, + + /// + /// Cyan RGB(0, 255, 255). + /// + /// + /// Same as . + /// + Cyan = Aqua, + + /// + /// Dark blue RGB(0, 0, 139). + /// + DarkBlue = 0x00008B, + + /// + /// Dark cyan RGB(0, 139, 139). + /// + DarkCyan = 0x008B8B, + + /// + /// Dark goldenrod RGB(184, 134, 11). + /// + DarkGoldenrod = 0xB8860B, + + /// + /// Dark gray RGB(169, 169, 169). + /// + DarkGray = 0xA9A9A9, + + /// + /// Dark green RGB(0, 100, 0). + /// + DarkGreen = 0x006400, + + /// + /// Dark grey RGB(169, 169, 169). + /// + /// + /// Same as . + /// + DarkGrey = DarkGray, + + /// + /// Dark khaki RGB(189, 183, 107). + /// + DarkKhaki = 0xBDB76B, + + /// + /// Dark magenta RGB(139, 0, 139). + /// + DarkMagenta = 0x8B008B, + + /// + /// Dark olive green RGB(85, 107, 47). + /// + DarkOliveGreen = 0x556B2F, + + /// + /// Dark orange RGB(255, 140, 0). + /// + DarkOrange = 0xFF8C00, + + /// + /// Dark orchid RGB(153, 50, 204). + /// + DarkOrchid = 0x9932CC, + + /// + /// Dark red RGB(139, 0, 0). + /// + DarkRed = 0x8B0000, + + /// + /// Dark salmon RGB(233, 150, 122). + /// + DarkSalmon = 0xE9967A, + + /// + /// Dark sea green RGB(143, 188, 143). + /// + DarkSeaGreen = 0x8FBC8F, + + /// + /// Dark slate blue RGB(72, 61, 139). + /// + DarkSlateBlue = 0x483D8B, + + /// + /// Dark slate gray RGB(47, 79, 79). + /// + DarkSlateGray = 0x2F4F4F, + + /// + /// Dark slate grey RGB(47, 79, 79). + /// + /// + /// Same as . + /// + DarkSlateGrey = DarkSlateGray, + + /// + /// Dark turquoise RGB(0, 206, 209). + /// + DarkTurquoise = 0x00CED1, + + /// + /// Dark violet RGB(148, 0, 211). + /// + DarkViolet = 0x9400D3, + + /// + /// Deep pink RGB(255, 20, 147). + /// + DeepPink = 0xFF1493, + + /// + /// Deep sky blue RGB(0, 191, 255). + /// + DeepSkyBlue = 0x00BFFF, + + /// + /// Dim gray RGB(105, 105, 105). + /// + DimGray = 0x696969, + + /// + /// Dim grey RGB(105, 105, 105). + /// + /// + /// Same as . + /// + DimGrey = DimGray, + + /// + /// Dodger blue RGB(30, 144, 255). + /// + DodgerBlue = 0x1E90FF, + + /// + /// Fire brick RGB(178, 34, 34). + /// + FireBrick = 0xB22222, + + /// + /// Floral white RGB(255, 250, 240). + /// + FloralWhite = 0xFFFAF0, + + /// + /// Forest green RGB(34, 139, 34). + /// + ForestGreen = 0x228B22, + + /// + /// Fuchsia RGB(255, 0, 255). + /// + /// + /// Same as . + /// + Fuchsia = Magenta, + + /// + /// Gainsboro RGB(220, 220, 220). + /// + Gainsboro = 0xDCDCDC, + + /// + /// Ghost white RGB(248, 248, 255). + /// + GhostWhite = 0xF8F8FF, + + /// + /// Gold RGB(255, 215, 0). + /// + Gold = 0xFFD700, + + /// + /// Goldenrod RGB(218, 165, 32). + /// + Goldenrod = 0xDAA520, + + /// + /// Gray RGB(128, 128, 128). + /// + Gray = 0x808080, + + /// + /// Green RGB(0, 128, 0). + /// + Green = 0x008000, + + /// + /// Green yellow RGB(173, 255, 47). + /// + GreenYellow = 0xADFF2F, + + /// + /// Grey RGB(128, 128, 128). + /// + /// + /// Same as . + /// + Grey = Gray, + + /// + /// Honey dew RGB(240, 255, 240). + /// + HoneyDew = 0xF0FFF0, + + /// + /// Hot pink RGB(255, 105, 180). + /// + HotPink = 0xFF69B4, + + /// + /// Indian red RGB(205, 92, 92). + /// + IndianRed = 0xCD5C5C, + + /// + /// Indigo RGB(75, 0, 130). + /// + Indigo = 0x4B0082, + + /// + /// Ivory RGB(255, 255, 240). + /// + Ivory = 0xFFFFF0, + + /// + /// Khaki RGB(240, 230, 140). + /// + Khaki = 0xF0E68C, + + /// + /// Lavender RGB(230, 230, 250). + /// + Lavender = 0xE6E6FA, + + /// + /// Lavender blush RGB(255, 240, 245). + /// + LavenderBlush = 0xFFF0F5, + + /// + /// Lawn green RGB(124, 252, 0). + /// + LawnGreen = 0x7CFC00, + + /// + /// Lemon chiffon RGB(255, 250, 205). + /// + LemonChiffon = 0xFFFACD, + + /// + /// Light blue RGB(173, 216, 230). + /// + LightBlue = 0xADD8E6, + + /// + /// Light coral RGB(240, 128, 128). + /// + LightCoral = 0xF08080, + + /// + /// Light cyan RGB(224, 255, 255). + /// + LightCyan = 0xE0FFFF, + + /// + /// Light goldenrod yellow RGB(250, 250, 210). + /// + LightGoldenrodYellow = 0xFAFAD2, + + /// + /// Light gray RGB(211, 211, 211). + /// + LightGray = 0xD3D3D3, + + /// + /// Light green RGB(144, 238, 144). + /// + LightGreen = 0x90EE90, + + /// + /// Light grey RGB(211, 211, 211). + /// + /// + /// Same as . + /// + LightGrey = LightGray, + + /// + /// Light pink RGB(255, 182, 193). + /// + LightPink = 0xFFB6C1, + + /// + /// Light salmon RGB(255, 160, 122). + /// + LightSalmon = 0xFFA07A, + + /// + /// Light sea green RGB(32, 178, 170). + /// + LightSeaGreen = 0x20B2AA, + + /// + /// Light sky blue RGB(135, 206, 250). + /// + LightSkyBlue = 0x87CEFA, + + /// + /// Light slate gray RGB(119, 136, 153). + /// + LightSlateGray = 0x778899, + + /// + /// Light slate grey RGB(119, 136, 153). + /// + /// + /// Same as . + /// + LightSlateGrey = LightSlateGray, + + /// + /// Light steel blue RGB(176, 196, 222). + /// + LightSteelBlue = 0xB0C4DE, + + /// + /// Light yellow RGB(255, 255, 224). + /// + LightYellow = 0xFFFFE0, + + /// + /// Lime RGB(0, 255, 0). + /// + Lime = 0x00FF00, + + /// + /// Lime green RGB(50, 205, 50). + /// + LimeGreen = 0x32CD32, + + /// + /// Linen RGB(250, 240, 230). + /// + Linen = 0xFAF0E6, + + /// + /// Magenta RGB(255, 0, 255). + /// + Magenta = 0xFF00FF, + + /// + /// Maroon RGB(128, 0, 0). + /// + Maroon = 0x800000, + + /// + /// Medium aqua marine RGB(102, 205, 170). + /// + MediumAquaMarine = 0x66CDAA, + + /// + /// Medium blue RGB(0, 0, 205). + /// + MediumBlue = 0x0000CD, + + /// + /// Medium orchid RGB(186, 85, 211). + /// + MediumOrchid = 0xBA55D3, + + /// + /// Medium purple RGB(147, 112, 219). + /// + MediumPurple = 0x9370DB, + + /// + /// Medium sea green RGB(60, 179, 113). + /// + MediumSeaGreen = 0x3CB371, + + /// + /// Medium slate blue RGB(123, 104, 238). + /// + MediumSlateBlue = 0x7B68EE, + + /// + /// Medium spring green RGB(0, 250, 154). + /// + MediumSpringGreen = 0x00FA9A, + + /// + /// Medium turquoise RGB(72, 209, 204). + /// + MediumTurquoise = 0x48D1CC, + + /// + /// Medium violet red RGB(199, 21, 133). + /// + MediumVioletRed = 0xC71585, + + /// + /// Midnight blue RGB(25, 25, 112). + /// + MidnightBlue = 0x191970, + + /// + /// Mint cream RGB(245, 255, 250). + /// + MintCream = 0xF5FFFA, + + /// + /// Misty rose RGB(255, 228, 225). + /// + MistyRose = 0xFFE4E1, + + /// + /// Moccasin RGB(255, 228, 181). + /// + Moccasin = 0xFFE4B5, + + /// + /// Navajo white RGB(255, 222, 173). + /// + NavajoWhite = 0xFFDEAD, + + /// + /// Navy RGB(0, 0, 128). + /// + Navy = 0x000080, + + /// + /// Old lace RGB(253, 245, 230). + /// + OldLace = 0xFDF5E6, + + /// + /// Olive RGB(128, 128, 0). + /// + Olive = 0x808000, + + /// + /// Olive drab RGB(107, 142, 35). + /// + OliveDrab = 0x6B8E23, + + /// + /// Orange RGB(255, 165, 0). + /// + Orange = 0xFFA500, + + /// + /// Orange red RGB(255, 69, 0). + /// + OrangeRed = 0xFF4500, + + /// + /// Orchid RGB(218, 112, 214). + /// + Orchid = 0xDA70D6, + + /// + /// Pale goldenrod RGB(238, 232, 170). + /// + PaleGoldenrod = 0xEEE8AA, + + /// + /// Pale green RGB(152, 251, 152). + /// + PaleGreen = 0x98FB98, + + /// + /// Pale turquoise RGB(175, 238, 238). + /// + PaleTurquoise = 0xAFEEEE, + + /// + /// Pale violet red RGB(219, 112, 147). + /// + PaleVioletRed = 0xDB7093, + + /// + /// Papaya whip RGB(255, 239, 213). + /// + PapayaWhip = 0xFFEFD5, + + /// + /// Peach puff RGB(255, 218, 185). + /// + PeachPuff = 0xFFDAB9, + + /// + /// Peru RGB(205, 133, 63). + /// + Peru = 0xCD853F, + + /// + /// Pink RGB(255, 192, 203). + /// + Pink = 0xFFC0CB, + + /// + /// Plum RGB(221, 160, 221). + /// + Plum = 0xDDA0DD, + + /// + /// Powder blue RGB(176, 224, 230). + /// + PowderBlue = 0xB0E0E6, + + /// + /// Purple RGB(128, 0, 128). + /// + Purple = 0x800080, + + /// + /// Rebecca purple RGB(102, 51, 153). + /// + RebeccaPurple = 0x663399, + + /// + /// Red RGB(255, 0, 0). + /// + Red = 0xFF0000, + + /// + /// Rosy brown RGB(188, 143, 143). + /// + RosyBrown = 0xBC8F8F, + + /// + /// Royal blue RGB(65, 105, 225). + /// + RoyalBlue = 0x4169E1, + + /// + /// Saddle brown RGB(139, 69, 19). + /// + SaddleBrown = 0x8B4513, + + /// + /// Salmon RGB(250, 128, 114). + /// + Salmon = 0xFA8072, + + /// + /// Sandy brown RGB(244, 164, 96). + /// + SandyBrown = 0xF4A460, + + /// + /// Sea green RGB(46, 139, 87). + /// + SeaGreen = 0x2E8B57, + + /// + /// Sea shell RGB(255, 245, 238). + /// + SeaShell = 0xFFF5EE, + + /// + /// Sienna RGB(160, 82, 45). + /// + Sienna = 0xA0522D, + + /// + /// Silver RGB(192, 192, 192). + /// + Silver = 0xC0C0C0, + + /// + /// Sky blue RGB(135, 206, 235). + /// + SkyBlue = 0x87CEEB, + + /// + /// Slate blue RGB(106, 90, 205). + /// + SlateBlue = 0x6A5ACD, + + /// + /// Slate gray RGB(112, 128, 144). + /// + SlateGray = 0x708090, + + /// + /// Slate grey RGB(112, 128, 144). + /// + /// + /// Same as . + /// + SlateGrey = SlateGray, + + /// + /// Snow RGB(255, 250, 250). + /// + Snow = 0xFFFAFA, + + /// + /// Spring green RGB(0, 255, 127). + /// + SpringGreen = 0x00FF7F, + + /// + /// Steel blue RGB(70, 130, 180). + /// + SteelBlue = 0x4682B4, + + /// + /// Tan RGB(210, 180, 140). + /// + Tan = 0xD2B48C, + + /// + /// Teal RGB(0, 128, 128). + /// + Teal = 0x008080, + + /// + /// Thistle RGB(216, 191, 216). + /// + Thistle = 0xD8BFD8, + + /// + /// Tomato RGB(255, 99, 71). + /// + Tomato = 0xFF6347, + + /// + /// Turquoise RGB(64, 224, 208). + /// + Turquoise = 0x40E0D0, + + /// + /// Violet RGB(238, 130, 238). + /// + Violet = 0xEE82EE, + + /// + /// Wheat RGB(245, 222, 179). + /// + Wheat = 0xF5DEB3, + + /// + /// White RGB(255, 255, 255). + /// + White = 0xFFFFFF, + + /// + /// White smoke RGB(245, 245, 245). + /// + WhiteSmoke = 0xF5F5F5, + + /// + /// Yellow RGB(255, 255, 0). + /// + Yellow = 0xFFFF00, + + /// + /// Yellow green RGB(154, 205, 50). + /// + YellowGreen = 0x9ACD32 +} diff --git a/Terminal.Gui/Drawing/Color/W3cColorNameResolver.cs b/Terminal.Gui/Drawing/Color/W3cColorNameResolver.cs new file mode 100644 index 0000000000..446e425726 --- /dev/null +++ b/Terminal.Gui/Drawing/Color/W3cColorNameResolver.cs @@ -0,0 +1,110 @@ +#nullable enable + +using System.Collections.Frozen; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; + +namespace Terminal.Gui; + +/// +/// W3C color name resolver. +/// +public class W3cColorNameResolver : IColorNameResolver +{ + /// + public IEnumerable GetColorNames () => + W3cColors.GetColorNames (); + + /// + public bool TryParseColor (ReadOnlySpan name, out Color color) => + W3cColors.TryParseColor (name, out color); + + /// + public bool TryNameColor (Color color, [NotNullWhen (true)] out string? name) => + W3cColors.TryNameColor (color, out name); +} + +/// +/// Helper class for transforming to and from enum. +/// +internal static class W3cColors +{ + private static readonly ImmutableArray Names; + private static readonly FrozenDictionary ArgbNameMap; + + static W3cColors () + { + // Populate based on names because enums with same numerical value + // are not otherwise distinguishable from each other. + string[] w3cNames = Enum.GetNames ().Order().ToArray(); + + Dictionary map = new(w3cNames.Length); + foreach (string name in w3cNames) + { + W3cColor w3c = Enum.Parse(name); + uint argb = GetArgb(w3c); + // TODO: Collect aliases? + _ = map.TryAdd (argb, name); + } + + Names = ImmutableArray.Create (w3cNames); + ArgbNameMap = map.ToFrozenDictionary (); + } + + /// + /// Gets read-only list of the W3C colors in alphabetical order. + /// + public static IReadOnlyList GetColorNames () + { + return Names; + } + + /// + /// Converts the given W3C color name to equivalent color value. + /// + /// W3C color name. + /// The successfully converted W3C color value. + /// True if the conversion succeeded; otherwise false. + public static bool TryParseColor (ReadOnlySpan name, out Color color) + { + if (!Enum.TryParse (name, ignoreCase: true, out W3cColor w3cColor) || + // Any numerical value converts to undefined enum value. + !Enum.IsDefined (w3cColor)) + { + color = default; + return false; + } + + uint argb = GetArgb (w3cColor); + color = new Color (argb); + return true; + } + + /// + /// Converts the given color value to a W3C color name. + /// + /// Color value to match W3C color. + /// The successfully converted W3C color name. + /// True if conversion succeeded; otherwise false. + public static bool TryNameColor (Color color, [NotNullWhen (true)] out string? name) + { + if (ArgbNameMap.TryGetValue (color.Argb, out name)) + { + return true; + } + + name = null; + return false; + } + + private static uint GetArgb (W3cColor w3cColor) + { + const int alphaShift = 24; + const uint alphaMask = 0xFFU << alphaShift; + + int rgb = (int)w3cColor; + + uint argb = (uint)rgb | alphaMask; + return argb; + } +} diff --git a/Terminal.Gui/Resources/Strings.Designer.cs b/Terminal.Gui/Resources/Strings.Designer.cs index e329b718e7..e88ae567cd 100644 --- a/Terminal.Gui/Resources/Strings.Designer.cs +++ b/Terminal.Gui/Resources/Strings.Designer.cs @@ -60,1320 +60,6 @@ internal Strings() { } } - /// - /// Looks up a localized string similar to Black. - /// - internal static string _000000 { - get { - return ResourceManager.GetString("#000000", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Navy. - /// - internal static string _000080 { - get { - return ResourceManager.GetString("#000080", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DarkBlue. - /// - internal static string _00008B { - get { - return ResourceManager.GetString("#00008B", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MediumBlue. - /// - internal static string _0000CD { - get { - return ResourceManager.GetString("#0000CD", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Blue. - /// - internal static string _0000FF { - get { - return ResourceManager.GetString("#0000FF", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DarkGreen. - /// - internal static string _006400 { - get { - return ResourceManager.GetString("#006400", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Green. - /// - internal static string _008000 { - get { - return ResourceManager.GetString("#008000", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Teal. - /// - internal static string _008080 { - get { - return ResourceManager.GetString("#008080", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DarkCyan. - /// - internal static string _008B8B { - get { - return ResourceManager.GetString("#008B8B", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DeepSkyBlue. - /// - internal static string _00BFFF { - get { - return ResourceManager.GetString("#00BFFF", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DarkTurquoise. - /// - internal static string _00CED1 { - get { - return ResourceManager.GetString("#00CED1", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MediumSpringGreen. - /// - internal static string _00FA9A { - get { - return ResourceManager.GetString("#00FA9A", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Lime. - /// - internal static string _00FF00 { - get { - return ResourceManager.GetString("#00FF00", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SpringGreen. - /// - internal static string _00FF7F { - get { - return ResourceManager.GetString("#00FF7F", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cyan. - /// - internal static string _00FFFF { - get { - return ResourceManager.GetString("#00FFFF", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to BrightGreen. - /// - internal static string _16C60C { - get { - return ResourceManager.GetString("#16C60C", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MidnightBlue. - /// - internal static string _191970 { - get { - return ResourceManager.GetString("#191970", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DodgerBlue. - /// - internal static string _1E90FF { - get { - return ResourceManager.GetString("#1E90FF", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to LightSeaGreen. - /// - internal static string _20B2AA { - get { - return ResourceManager.GetString("#20B2AA", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to ForestGreen. - /// - internal static string _228B22 { - get { - return ResourceManager.GetString("#228B22", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SeaGreen. - /// - internal static string _2E8B57 { - get { - return ResourceManager.GetString("#2E8B57", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DarkSlateGrey. - /// - internal static string _2F4F4F { - get { - return ResourceManager.GetString("#2F4F4F", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to LimeGreen. - /// - internal static string _32CD32 { - get { - return ResourceManager.GetString("#32CD32", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to BrightBlue. - /// - internal static string _3B78FF { - get { - return ResourceManager.GetString("#3B78FF", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MediumSeaGreen. - /// - internal static string _3CB371 { - get { - return ResourceManager.GetString("#3CB371", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Turquoise. - /// - internal static string _40E0D0 { - get { - return ResourceManager.GetString("#40E0D0", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to RoyalBlue. - /// - internal static string _4169E1 { - get { - return ResourceManager.GetString("#4169E1", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SteelBlue. - /// - internal static string _4682B4 { - get { - return ResourceManager.GetString("#4682B4", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DarkSlateBlue. - /// - internal static string _483D8B { - get { - return ResourceManager.GetString("#483D8B", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MediumTurquoise. - /// - internal static string _48D1CC { - get { - return ResourceManager.GetString("#48D1CC", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Indigo. - /// - internal static string _4B0082 { - get { - return ResourceManager.GetString("#4B0082", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DarkOliveGreen. - /// - internal static string _556B2F { - get { - return ResourceManager.GetString("#556B2F", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to CadetBlue. - /// - internal static string _5F9EA0 { - get { - return ResourceManager.GetString("#5F9EA0", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to BrightCyan. - /// - internal static string _61D6D6 { - get { - return ResourceManager.GetString("#61D6D6", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to CornflowerBlue. - /// - internal static string _6495ED { - get { - return ResourceManager.GetString("#6495ED", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to RebeccaPurple. - /// - internal static string _663399 { - get { - return ResourceManager.GetString("#663399", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MediumAquaMarine. - /// - internal static string _66CDAA { - get { - return ResourceManager.GetString("#66CDAA", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DimGray. - /// - internal static string _696969 { - get { - return ResourceManager.GetString("#696969", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SlateBlue. - /// - internal static string _6A5ACD { - get { - return ResourceManager.GetString("#6A5ACD", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to OliveDrab. - /// - internal static string _6B8E23 { - get { - return ResourceManager.GetString("#6B8E23", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SlateGray. - /// - internal static string _708090 { - get { - return ResourceManager.GetString("#708090", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DarkGray. - /// - internal static string _767676 { - get { - return ResourceManager.GetString("#767676", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to LightSlateGrey. - /// - internal static string _778899 { - get { - return ResourceManager.GetString("#778899", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MediumSlateBlue. - /// - internal static string _7B68EE { - get { - return ResourceManager.GetString("#7B68EE", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to LawnGreen. - /// - internal static string _7CFC00 { - get { - return ResourceManager.GetString("#7CFC00", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Chartreuse. - /// - internal static string _7FFF00 { - get { - return ResourceManager.GetString("#7FFF00", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Aquamarine. - /// - internal static string _7FFFD4 { - get { - return ResourceManager.GetString("#7FFFD4", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Maroon. - /// - internal static string _800000 { - get { - return ResourceManager.GetString("#800000", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Purple. - /// - internal static string _800080 { - get { - return ResourceManager.GetString("#800080", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Olive. - /// - internal static string _808000 { - get { - return ResourceManager.GetString("#808000", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Gray. - /// - internal static string _808080 { - get { - return ResourceManager.GetString("#808080", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SkyBlue. - /// - internal static string _87CEEB { - get { - return ResourceManager.GetString("#87CEEB", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to LightSkyBlue. - /// - internal static string _87CEFA { - get { - return ResourceManager.GetString("#87CEFA", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to BlueViolet. - /// - internal static string _8A2BE2 { - get { - return ResourceManager.GetString("#8A2BE2", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DarkRed. - /// - internal static string _8B0000 { - get { - return ResourceManager.GetString("#8B0000", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DarkMagenta. - /// - internal static string _8B008B { - get { - return ResourceManager.GetString("#8B008B", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SaddleBrown. - /// - internal static string _8B4513 { - get { - return ResourceManager.GetString("#8B4513", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DarkSeaGreen. - /// - internal static string _8FBC8F { - get { - return ResourceManager.GetString("#8FBC8F", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to LightGreen. - /// - internal static string _90EE90 { - get { - return ResourceManager.GetString("#90EE90", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MediumPurple. - /// - internal static string _9370DB { - get { - return ResourceManager.GetString("#9370DB", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DarkViolet. - /// - internal static string _9400D3 { - get { - return ResourceManager.GetString("#9400D3", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to PaleGreen. - /// - internal static string _98FB98 { - get { - return ResourceManager.GetString("#98FB98", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DarkOrchid. - /// - internal static string _9932CC { - get { - return ResourceManager.GetString("#9932CC", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to YellowGreen. - /// - internal static string _9ACD32 { - get { - return ResourceManager.GetString("#9ACD32", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Sienna. - /// - internal static string _A0522D { - get { - return ResourceManager.GetString("#A0522D", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Brown. - /// - internal static string _A52A2A { - get { - return ResourceManager.GetString("#A52A2A", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DarkGrey. - /// - internal static string _A9A9A9 { - get { - return ResourceManager.GetString("#A9A9A9", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to LightBlue. - /// - internal static string _ADD8E6 { - get { - return ResourceManager.GetString("#ADD8E6", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to GreenYellow. - /// - internal static string _ADFF2F { - get { - return ResourceManager.GetString("#ADFF2F", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to PaleTurquoise. - /// - internal static string _AFEEEE { - get { - return ResourceManager.GetString("#AFEEEE", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to LightSteelBlue. - /// - internal static string _B0C4DE { - get { - return ResourceManager.GetString("#B0C4DE", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to PowderBlue. - /// - internal static string _B0E0E6 { - get { - return ResourceManager.GetString("#B0E0E6", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to FireBrick. - /// - internal static string _B22222 { - get { - return ResourceManager.GetString("#B22222", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to BrightMagenta. - /// - internal static string _B4009E { - get { - return ResourceManager.GetString("#B4009E", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DarkGoldenRod. - /// - internal static string _B8860B { - get { - return ResourceManager.GetString("#B8860B", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MediumOrchid. - /// - internal static string _BA55D3 { - get { - return ResourceManager.GetString("#BA55D3", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to RosyBrown. - /// - internal static string _BC8F8F { - get { - return ResourceManager.GetString("#BC8F8F", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DarkKhaki. - /// - internal static string _BDB76B { - get { - return ResourceManager.GetString("#BDB76B", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Silver. - /// - internal static string _C0C0C0 { - get { - return ResourceManager.GetString("#C0C0C0", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MediumVioletRed. - /// - internal static string _C71585 { - get { - return ResourceManager.GetString("#C71585", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to IndianRed. - /// - internal static string _CD5C5C { - get { - return ResourceManager.GetString("#CD5C5C", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Peru. - /// - internal static string _CD853F { - get { - return ResourceManager.GetString("#CD853F", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Chocolate. - /// - internal static string _D2691E { - get { - return ResourceManager.GetString("#D2691E", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Tan. - /// - internal static string _D2B48C { - get { - return ResourceManager.GetString("#D2B48C", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to LightGray. - /// - internal static string _D3D3D3 { - get { - return ResourceManager.GetString("#D3D3D3", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Thistle. - /// - internal static string _D8BFD8 { - get { - return ResourceManager.GetString("#D8BFD8", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Orchid. - /// - internal static string _DA70D6 { - get { - return ResourceManager.GetString("#DA70D6", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to GoldenRod. - /// - internal static string _DAA520 { - get { - return ResourceManager.GetString("#DAA520", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to PaleVioletRed. - /// - internal static string _DB7093 { - get { - return ResourceManager.GetString("#DB7093", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Crimson. - /// - internal static string _DC143C { - get { - return ResourceManager.GetString("#DC143C", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Gainsboro. - /// - internal static string _DCDCDC { - get { - return ResourceManager.GetString("#DCDCDC", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Plum. - /// - internal static string _DDA0DD { - get { - return ResourceManager.GetString("#DDA0DD", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to BurlyWood. - /// - internal static string _DEB887 { - get { - return ResourceManager.GetString("#DEB887", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to LightCyan. - /// - internal static string _E0FFFF { - get { - return ResourceManager.GetString("#E0FFFF", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Lavender. - /// - internal static string _E6E6FA { - get { - return ResourceManager.GetString("#E6E6FA", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to BrightRed. - /// - internal static string _E74856 { - get { - return ResourceManager.GetString("#E74856", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DarkSalmon. - /// - internal static string _E9967A { - get { - return ResourceManager.GetString("#E9967A", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Violet. - /// - internal static string _EE82EE { - get { - return ResourceManager.GetString("#EE82EE", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to PaleGoldenRod. - /// - internal static string _EEE8AA { - get { - return ResourceManager.GetString("#EEE8AA", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to LightCoral. - /// - internal static string _F08080 { - get { - return ResourceManager.GetString("#F08080", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Khaki. - /// - internal static string _F0E68C { - get { - return ResourceManager.GetString("#F0E68C", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AliceBlue. - /// - internal static string _F0F8FF { - get { - return ResourceManager.GetString("#F0F8FF", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to HoneyDew. - /// - internal static string _F0FFF0 { - get { - return ResourceManager.GetString("#F0FFF0", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Azure. - /// - internal static string _F0FFFF { - get { - return ResourceManager.GetString("#F0FFFF", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SandyBrown. - /// - internal static string _F4A460 { - get { - return ResourceManager.GetString("#F4A460", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Wheat. - /// - internal static string _F5DEB3 { - get { - return ResourceManager.GetString("#F5DEB3", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Beige. - /// - internal static string _F5F5DC { - get { - return ResourceManager.GetString("#F5F5DC", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to WhiteSmoke. - /// - internal static string _F5F5F5 { - get { - return ResourceManager.GetString("#F5F5F5", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MintCream. - /// - internal static string _F5FFFA { - get { - return ResourceManager.GetString("#F5FFFA", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to GhostWhite. - /// - internal static string _F8F8FF { - get { - return ResourceManager.GetString("#F8F8FF", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to BrightYellow. - /// - internal static string _F9F1A5 { - get { - return ResourceManager.GetString("#F9F1A5", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Salmon. - /// - internal static string _FA8072 { - get { - return ResourceManager.GetString("#FA8072", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to AntiqueWhite. - /// - internal static string _FAEBD7 { - get { - return ResourceManager.GetString("#FAEBD7", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Linen. - /// - internal static string _FAF0E6 { - get { - return ResourceManager.GetString("#FAF0E6", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to LightGoldenRodYellow. - /// - internal static string _FAFAD2 { - get { - return ResourceManager.GetString("#FAFAD2", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to OldLace. - /// - internal static string _FDF5E6 { - get { - return ResourceManager.GetString("#FDF5E6", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Red. - /// - internal static string _FF0000 { - get { - return ResourceManager.GetString("#FF0000", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Magenta. - /// - internal static string _FF00FF { - get { - return ResourceManager.GetString("#FF00FF", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DeepPink. - /// - internal static string _FF1493 { - get { - return ResourceManager.GetString("#FF1493", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to OrangeRed. - /// - internal static string _FF4500 { - get { - return ResourceManager.GetString("#FF4500", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Tomato. - /// - internal static string _FF6347 { - get { - return ResourceManager.GetString("#FF6347", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to HotPink. - /// - internal static string _FF69B4 { - get { - return ResourceManager.GetString("#FF69B4", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Coral. - /// - internal static string _FF7F50 { - get { - return ResourceManager.GetString("#FF7F50", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to DarkOrange. - /// - internal static string _FF8C00 { - get { - return ResourceManager.GetString("#FF8C00", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to LightSalmon. - /// - internal static string _FFA07A { - get { - return ResourceManager.GetString("#FFA07A", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Orange. - /// - internal static string _FFA500 { - get { - return ResourceManager.GetString("#FFA500", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to LightPink. - /// - internal static string _FFB6C1 { - get { - return ResourceManager.GetString("#FFB6C1", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Pink. - /// - internal static string _FFC0CB { - get { - return ResourceManager.GetString("#FFC0CB", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Gold. - /// - internal static string _FFD700 { - get { - return ResourceManager.GetString("#FFD700", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to PeachPuff. - /// - internal static string _FFDAB9 { - get { - return ResourceManager.GetString("#FFDAB9", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to NavajoWhite. - /// - internal static string _FFDEAD { - get { - return ResourceManager.GetString("#FFDEAD", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Moccasin. - /// - internal static string _FFE4B5 { - get { - return ResourceManager.GetString("#FFE4B5", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Bisque. - /// - internal static string _FFE4C4 { - get { - return ResourceManager.GetString("#FFE4C4", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to MistyRose. - /// - internal static string _FFE4E1 { - get { - return ResourceManager.GetString("#FFE4E1", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to BlanchedAlmond. - /// - internal static string _FFEBCD { - get { - return ResourceManager.GetString("#FFEBCD", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to PapayaWhip. - /// - internal static string _FFEFD5 { - get { - return ResourceManager.GetString("#FFEFD5", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to LavenderBlush. - /// - internal static string _FFF0F5 { - get { - return ResourceManager.GetString("#FFF0F5", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to SeaShell. - /// - internal static string _FFF5EE { - get { - return ResourceManager.GetString("#FFF5EE", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Cornsilk. - /// - internal static string _FFF8DC { - get { - return ResourceManager.GetString("#FFF8DC", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to LemonChiffon. - /// - internal static string _FFFACD { - get { - return ResourceManager.GetString("#FFFACD", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to FloralWhite. - /// - internal static string _FFFAF0 { - get { - return ResourceManager.GetString("#FFFAF0", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Snow. - /// - internal static string _FFFAFA { - get { - return ResourceManager.GetString("#FFFAFA", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Yellow. - /// - internal static string _FFFF00 { - get { - return ResourceManager.GetString("#FFFF00", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to LightYellow. - /// - internal static string _FFFFE0 { - get { - return ResourceManager.GetString("#FFFFE0", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Ivory. - /// - internal static string _FFFFF0 { - get { - return ResourceManager.GetString("#FFFFF0", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to White. - /// - internal static string _FFFFFF { - get { - return ResourceManager.GetString("#FFFFFF", resourceCulture); - } - } - /// /// Looks up a localized string similar to _Cancel. /// diff --git a/Terminal.Gui/Resources/Strings.resx b/Terminal.Gui/Resources/Strings.resx index 773ee55937..e241b11f8e 100644 --- a/Terminal.Gui/Resources/Strings.resx +++ b/Terminal.Gui/Resources/Strings.resx @@ -280,444 +280,6 @@ Date Picker - - AliceBlue - - - AntiqueWhite - - - Aquamarine - - - Azure - - - Beige - - - Bisque - - - Black - - - BlanchedAlmond - - - Blue - - - BlueViolet - - - Brown - - - BurlyWood - - - CadetBlue - - - Chartreuse - - - Chocolate - - - Coral - - - CornflowerBlue - - - Cornsilk - - - Crimson - - - Cyan - - - DarkBlue - - - DarkCyan - - - DarkGoldenRod - - - DarkGrey - - - DarkGreen - - - DarkKhaki - - - DarkMagenta - - - DarkOliveGreen - - - DarkOrange - - - DarkOrchid - - - DarkRed - - - DarkSalmon - - - DarkSeaGreen - - - DarkSlateBlue - - - DarkSlateGrey - - - DarkTurquoise - - - DarkViolet - - - DeepPink - - - DeepSkyBlue - - - DimGray - - - DodgerBlue - - - FireBrick - - - FloralWhite - - - ForestGreen - - - Gainsboro - - - GhostWhite - - - Gold - - - GoldenRod - - - Gray - - - Green - - - GreenYellow - - - HoneyDew - - - HotPink - - - IndianRed - - - Indigo - - - Ivory - - - Khaki - - - Lavender - - - LavenderBlush - - - LawnGreen - - - LemonChiffon - - - LightBlue - - - LightCoral - - - LightCyan - - - LightGoldenRodYellow - - - LightGray - - - LightGreen - - - LightPink - - - LightSalmon - - - LightSeaGreen - - - LightSkyBlue - - - LightSlateGrey - - - LightSteelBlue - - - LightYellow - - - Lime - - - LimeGreen - - - Linen - - - Magenta - - - Maroon - - - MediumAquaMarine - - - MediumBlue - - - MediumOrchid - - - MediumPurple - - - MediumSeaGreen - - - MediumSlateBlue - - - MediumSpringGreen - - - MediumTurquoise - - - MediumVioletRed - - - MidnightBlue - - - MintCream - - - MistyRose - - - Moccasin - - - NavajoWhite - - - Navy - - - OldLace - - - Olive - - - OliveDrab - - - Orange - - - OrangeRed - - - Orchid - - - PaleGoldenRod - - - PaleGreen - - - PaleTurquoise - - - PaleVioletRed - - - PapayaWhip - - - PeachPuff - - - Peru - - - Pink - - - Plum - - - PowderBlue - - - Purple - - - RebeccaPurple - - - Red - - - RosyBrown - - - RoyalBlue - - - SaddleBrown - - - Salmon - - - SandyBrown - - - SeaGreen - - - SeaShell - - - Sienna - - - Silver - - - SkyBlue - - - SlateBlue - - - SlateGray - - - Snow - - - SpringGreen - - - SteelBlue - - - Tan - - - Teal - - - Thistle - - - Tomato - - - Turquoise - - - Violet - - - Wheat - - - White - - - WhiteSmoke - - - Yellow - - - YellowGreen - - - BrightBlue - - - BrightCyan - - - BrightRed - - - BrightGreen - - - BrightMagenta - - - BrightYellow - - - DarkGray - Co_lors diff --git a/Terminal.Gui/Views/ColorPicker.cs b/Terminal.Gui/Views/ColorPicker.cs index 415fc1acd1..2bd102085c 100644 --- a/Terminal.Gui/Views/ColorPicker.cs +++ b/Terminal.Gui/Views/ColorPicker.cs @@ -1,7 +1,5 @@ #nullable enable -using System; - namespace Terminal.Gui; /// @@ -34,7 +32,7 @@ public ColorPicker () private Color _selectedColor = Color.Black; // TODO: Add interface - private readonly IColorNameResolver _colorNameResolver = new W3CColors (); + private readonly IColorNameResolver _colorNameResolver = new MultiStandardColorNameResolver (); private List _bars = new (); @@ -64,7 +62,7 @@ public void ApplyStyleChanges () Width = textFieldWidth }; tfValue.HasFocusChanged += UpdateSingleBarValueFromTextField; - tfValue.Accepting += (s, _)=>UpdateSingleBarValueFromTextField(s); + tfValue.Accepting += (s, _) => UpdateSingleBarValueFromTextField (s); _textFields.Add (bar, tfValue); } @@ -182,7 +180,7 @@ private void CreateTextField () Add (_tfHex); _tfHex.HasFocusChanged += UpdateValueFromTextField; - _tfHex.Accepting += (_,_)=> UpdateValueFromTextField(); + _tfHex.Accepting += (_, _) => UpdateValueFromTextField (); } private void DisposeOldViews () @@ -266,7 +264,7 @@ private void SyncSubViewValues (bool syncBars) if (_tfName != null) { - _tfName.Text = _colorNameResolver.TryNameColor (_selectedColor, out string name) ? name : string.Empty; + _tfName.Text = _colorNameResolver.TryNameColor (_selectedColor, out string? name) ? name : string.Empty; } if (_tfHex != null) @@ -312,7 +310,7 @@ private void UpdateValueFromName (object? sender, HasFocusEventArgs e) } // it is a leave event so update - UpdateValueFromName(); + UpdateValueFromName (); } private void UpdateValueFromName () { diff --git a/Tests/UnitTests/Resources/ResourceManagerTests.cs b/Tests/UnitTests/Resources/ResourceManagerTests.cs index 1c49b3447f..15ebb0bdda 100644 --- a/Tests/UnitTests/Resources/ResourceManagerTests.cs +++ b/Tests/UnitTests/Resources/ResourceManagerTests.cs @@ -9,10 +9,6 @@ namespace Terminal.Gui.ResourcesTests; public class ResourceManagerTests { - private const string DODGER_BLUE_COLOR_KEY = "DodgerBlue"; - private const string DODGER_BLUE_COLOR_NAME = "DodgerBlue"; - private const string NO_NAMED_COLOR_KEY = "#1E80FF"; - private const string NO_NAMED_COLOR_NAME = "#1E80FF"; private const string EXISTENT_CULTURE = "pt-PT"; private const string NO_EXISTENT_CULTURE = "de-DE"; private const string NO_EXISTENT_KEY = "blabla"; @@ -50,51 +46,6 @@ public void GetObject_FallBack_To_Default_For_Not_Translated_Existent_Culture_Fi RestoreCurrentCultures (); } - [Fact] - public void GetResourceSet_FallBack_To_Default_For_No_Existent_Culture_File () - { - CultureInfo.CurrentCulture = new (NO_EXISTENT_CULTURE); - CultureInfo.CurrentUICulture = new (NO_EXISTENT_CULTURE); - - // W3CColors.GetColorNames also calls ColorStrings.GetW3CColorNames - string [] colorNames = new W3CColors ().GetColorNames ().ToArray (); - Assert.Contains (DODGER_BLUE_COLOR_NAME, colorNames); - Assert.DoesNotContain (NO_TRANSLATED_VALUE, colorNames); - - RestoreCurrentCultures (); - } - - [Fact] - public void GetResourceSet_FallBack_To_Default_For_Not_Translated_Existent_Culture_File () - { - CultureInfo.CurrentCulture = new (EXISTENT_CULTURE); - CultureInfo.CurrentUICulture = new (EXISTENT_CULTURE); - - // These aren't already translated - // ColorStrings.GetW3CColorNames method uses GetResourceSet method to retrieve color names - IEnumerable colorNames = ColorStrings.GetW3CColorNames (); - Assert.NotEmpty (colorNames); - - // W3CColors.GetColorNames also calls ColorStrings.GetW3CColorNames - colorNames = new W3CColors ().GetColorNames ().ToArray (); - Assert.Contains (DODGER_BLUE_COLOR_NAME, colorNames); - Assert.DoesNotContain (NO_TRANSLATED_VALUE, colorNames); - - // ColorStrings.TryParseW3CColorName method uses GetResourceSet method to retrieve a color value - Assert.True (ColorStrings.TryParseW3CColorName (DODGER_BLUE_COLOR_NAME, out Color color)); - Assert.Equal (DODGER_BLUE_COLOR_KEY, color.ToString ()); - - // W3CColors.GetColorNames also calls ColorStrings.GetW3CColorNames for no-named colors - colorNames = new W3CColors ().GetColorNames ().ToArray (); - Assert.DoesNotContain (NO_NAMED_COLOR_NAME, colorNames); - - // ColorStrings.TryParseW3CColorName method uses GetResourceSet method to retrieve a color value for no-named colors - Assert.True (ColorStrings.TryParseW3CColorName (NO_NAMED_COLOR_NAME, out color)); - Assert.Equal (NO_NAMED_COLOR_KEY, color.ToString ()); - - RestoreCurrentCultures (); - } - [Fact] public void GetResourceSet_With_Filter_Does_Not_Overflows_If_Key_Does_Not_Exist () { diff --git a/Tests/UnitTests/Views/ColorPickerTests.cs b/Tests/UnitTests/Views/ColorPickerTests.cs index 121696ff1d..5de5cb0bcf 100644 --- a/Tests/UnitTests/Views/ColorPickerTests.cs +++ b/Tests/UnitTests/Views/ColorPickerTests.cs @@ -749,7 +749,15 @@ public void ColorPicker_TabCompleteColorName () // Auto complete the color name Application.RaiseKeyDownEvent (Key.Tab); - Assert.Equal ("Aquamarine", name.Text); + // Match cyan alternative name + Assert.Equal ("Aqua", name.Text); + + Assert.True (name.HasFocus); + + Application.RaiseKeyDownEvent (Key.Tab); + + // Resolves to cyan color + Assert.Equal ("Cyan", name.Text); // Tab out of the text field Application.RaiseKeyDownEvent (Key.Tab); @@ -757,7 +765,7 @@ public void ColorPicker_TabCompleteColorName () Assert.False (name.HasFocus); Assert.NotSame (name, cp.Focused); - Assert.Equal ("#7FFFD4", hex.Text); + Assert.Equal ("#00FFFF", hex.Text); Application.Top?.Dispose (); Application.ResetState (true); @@ -817,14 +825,6 @@ public static IEnumerable ColorPickerTestData_WithTextFields () }; } - [Fact] - public void TestColorNames () - { - var colors = new W3CColors (); - Assert.Contains ("Aquamarine", colors.GetColorNames ()); - Assert.DoesNotContain ("Save as", colors.GetColorNames ()); - } - private ColorBar GetColorBar (ColorPicker cp, ColorPickerPart toGet) { if (toGet <= ColorPickerPart.Bar3) @@ -845,7 +845,7 @@ private ColorPicker GetColorPicker (ColorModel colorModel, bool showTextFields, Application.Navigation = new (); - Application.Top = new() { Width = 20, Height = 5 }; + Application.Top = new () { Width = 20, Height = 5 }; Application.Top.Add (cp); Application.Top.LayoutSubViews (); diff --git a/Tests/UnitTestsParallelizable/Drawing/Color/AnsiColorNameResolverTests.cs b/Tests/UnitTestsParallelizable/Drawing/Color/AnsiColorNameResolverTests.cs new file mode 100644 index 0000000000..76e8e34bfe --- /dev/null +++ b/Tests/UnitTestsParallelizable/Drawing/Color/AnsiColorNameResolverTests.cs @@ -0,0 +1,124 @@ +#nullable enable + +namespace Terminal.Gui.DrawingTests; + +public class AnsiColorNameResolverTests +{ + private readonly AnsiColorNameResolver _candidate = new(); + + [Fact] + public void GetNames_Returns16ColorNames () + { + string[] expected = Enum.GetNames(); + + string[] actual = _candidate.GetColorNames ().ToArray(); + + Assert.Equal (expected, actual); + } + + [Theory] + [InlineData (0, 0, 0, nameof (ColorName16.Black))] + [InlineData (0, 0, 255, nameof (ColorName16.Blue))] + [InlineData (59, 120, 255, nameof (ColorName16.BrightBlue))] + [InlineData (97, 214, 214, nameof (ColorName16.BrightCyan))] + [InlineData (22, 198, 12, nameof (ColorName16.BrightGreen))] + [InlineData (180, 0, 158, nameof (ColorName16.BrightMagenta))] + [InlineData (231, 72, 86, nameof (ColorName16.BrightRed))] + [InlineData (249, 241, 165, nameof (ColorName16.BrightYellow))] + [InlineData (0, 255, 255, nameof (ColorName16.Cyan))] + [InlineData (118, 118, 118, nameof (ColorName16.DarkGray))] + [InlineData (128, 128, 128, nameof (ColorName16.Gray))] + [InlineData (0, 128, 0, nameof (ColorName16.Green))] + [InlineData (255, 0, 255, nameof (ColorName16.Magenta))] + [InlineData (255, 0, 0, nameof (ColorName16.Red))] + [InlineData (255, 255, 255, nameof (ColorName16.White))] + [InlineData (255, 255, 0, nameof (ColorName16.Yellow))] + public void TryNameColor_ReturnsExpectedColorName (byte r, byte g, byte b, string expectedName) + { + var expected = (true, expectedName); + + bool actualSuccess = _candidate.TryNameColor(new Color(r, g, b), out string? actualName); + var actual = (actualSuccess, actualName); + + Assert.Equal (expected, actual); + } + + [Fact] + public void TryNameColor_NoMatchFails () + { + (bool, string?) expected = (false, null); + + bool actualSuccess = _candidate.TryNameColor (new Color (1, 2, 3), out string? actualName); + var actual = (actualSuccess, actualName); + + Assert.Equal (expected, actual); + } + + [Theory] + [InlineData (nameof (ColorName16.Black), 0, 0, 0)] + [InlineData (nameof (ColorName16.Blue), 0, 0, 255)] + [InlineData (nameof (ColorName16.BrightBlue), 59, 120, 255)] + [InlineData (nameof (ColorName16.BrightCyan), 97, 214, 214)] + [InlineData (nameof (ColorName16.BrightGreen), 22, 198, 12)] + [InlineData (nameof (ColorName16.BrightMagenta), 180, 0, 158)] + [InlineData (nameof (ColorName16.BrightRed), 231, 72, 86)] + [InlineData (nameof (ColorName16.BrightYellow), 249, 241, 165)] + [InlineData (nameof (ColorName16.Cyan), 0, 255, 255)] + [InlineData (nameof (ColorName16.DarkGray), 118, 118, 118)] + [InlineData (nameof (ColorName16.Gray), 128, 128, 128)] + [InlineData (nameof (ColorName16.Green), 0, 128, 0)] + [InlineData (nameof (ColorName16.Magenta), 255, 0, 255)] + [InlineData (nameof (ColorName16.Red), 255, 0, 0)] + [InlineData (nameof (ColorName16.White), 255, 255, 255)] + [InlineData (nameof (ColorName16.Yellow), 255, 255, 0)] + // Case-insensitive + [InlineData ("BRIGHTBLUE", 59, 120, 255)] + [InlineData ("brightblue", 59, 120, 255)] + public void TryParseColor_ReturnsExpectedColor (string inputName, byte r, byte g, byte b) + { + var expected = (true, new Color(r, g, b)); + + bool actualSuccess = _candidate.TryParseColor (inputName, out Color actualColor); + var actual = (actualSuccess, actualColor); + + Assert.Equal (expected, actual); + } + + [Theory] + [InlineData ("12", 231, 72, 86)] // ColorName16.BrightRed + public void TryParseColor_ResolvesValidEnumNumber (string inputName, byte r, byte g, byte b) + { + var expected = (true, new Color(r, g, b)); + + bool actualSuccess = _candidate.TryParseColor (inputName, out Color actualColor); + var actual = (actualSuccess, actualColor); + + Assert.Equal (expected, actual); + } + + [Theory] + [InlineData (null)] + [InlineData ("")] + [InlineData ("brightlight")] + public void TryParseColor_FailsOnInvalidColorName (string? invalidName) + { + var expected = (false, default(Color)); + + bool actualSuccess = _candidate.TryParseColor (invalidName, out Color actualColor); + var actual = (actualSuccess, actualColor); + + Assert.Equal (expected, actual); + } + + [Theory] + [InlineData ("-12")] + public void TryParseColor_FailsOnInvalidEnumNumber (string invalidName) + { + var expected = (false, default(Color)); + + bool actualSuccess = _candidate.TryParseColor (invalidName, out Color actualColor); + var actual = (actualSuccess, actualColor); + + Assert.Equal (expected, actual); + } +} diff --git a/Tests/UnitTestsParallelizable/Drawing/Color/MultiStandardColorNameResolverTests.cs b/Tests/UnitTestsParallelizable/Drawing/Color/MultiStandardColorNameResolverTests.cs new file mode 100644 index 0000000000..0e7677d659 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Drawing/Color/MultiStandardColorNameResolverTests.cs @@ -0,0 +1,232 @@ +#nullable enable + +namespace Terminal.Gui.DrawingTests; + +public class MultiStandardColorNameResolverTests +{ + private readonly MultiStandardColorNameResolver _candidate = new(); + + [Theory] + // ANSI color names. + [InlineData (nameof (ColorName16.Black))] + [InlineData (nameof (ColorName16.White))] + [InlineData (nameof (ColorName16.Red))] + [InlineData (nameof (ColorName16.Green))] + [InlineData (nameof (ColorName16.Blue))] + [InlineData (nameof (ColorName16.Cyan))] + [InlineData (nameof (ColorName16.Magenta))] + [InlineData (nameof (ColorName16.DarkGray))] + [InlineData (nameof (ColorName16.BrightGreen))] + [InlineData (nameof (ColorName16.BrightMagenta))] + // Regular W3C color. + [InlineData (nameof (W3cColor.AliceBlue))] + [InlineData (nameof (W3cColor.BlanchedAlmond))] + [InlineData (nameof (W3cColor.CadetBlue))] + [InlineData (nameof (W3cColor.DarkBlue))] + [InlineData (nameof (W3cColor.FireBrick))] + [InlineData (nameof (W3cColor.Gainsboro))] + [InlineData (nameof (W3cColor.HoneyDew))] + [InlineData (nameof (W3cColor.Indigo))] + [InlineData (nameof (W3cColor.Khaki))] + [InlineData (nameof (W3cColor.Lavender))] + [InlineData (nameof (W3cColor.Maroon))] + [InlineData (nameof (W3cColor.Navy))] + [InlineData (nameof (W3cColor.Olive))] + [InlineData (nameof (W3cColor.Plum))] + [InlineData (nameof (W3cColor.RoyalBlue))] + [InlineData (nameof (W3cColor.Silver))] + [InlineData (nameof (W3cColor.Tomato))] + [InlineData (nameof (W3cColor.Violet))] + [InlineData (nameof (W3cColor.WhiteSmoke))] + [InlineData (nameof (W3cColor.YellowGreen))] + // W3C alternatives. + [InlineData (nameof (W3cColor.Grey))] + [InlineData (nameof (W3cColor.DarkGrey))] + [InlineData (nameof (W3cColor.Aqua))] + [InlineData (nameof (W3cColor.Fuchsia))] + [InlineData (nameof (W3cColor.DarkSlateGray))] + [InlineData (nameof (W3cColor.DarkSlateGrey))] + [InlineData (nameof (W3cColor.DimGray))] + [InlineData (nameof (W3cColor.DimGrey))] + [InlineData (nameof (W3cColor.LightGray))] + [InlineData (nameof (W3cColor.LightGrey))] + [InlineData (nameof (W3cColor.SlateGray))] + [InlineData (nameof (W3cColor.SlateGrey))] + public void GetNames_ContainsCombinationOfAnsiAndW3cNames (string name) + { + string[] names = _candidate.GetColorNames ().ToArray(); + Assert.Contains (name, names); + } + + [Theory] + // ANSI color names + [InlineData (0, 0, 0, nameof (ColorName16.Black))] + [InlineData (0, 0, 255, nameof (ColorName16.Blue))] + [InlineData (59, 120, 255, nameof (ColorName16.BrightBlue))] + [InlineData (97, 214, 214, nameof (ColorName16.BrightCyan))] + [InlineData (22, 198, 12, nameof (ColorName16.BrightGreen))] + [InlineData (180, 0, 158, nameof (ColorName16.BrightMagenta))] + [InlineData (231, 72, 86, nameof (ColorName16.BrightRed))] + [InlineData (249, 241, 165, nameof (ColorName16.BrightYellow))] + [InlineData (0, 255, 255, nameof (ColorName16.Cyan))] + [InlineData (118, 118, 118, nameof (ColorName16.DarkGray))] + [InlineData (128, 128, 128, nameof (ColorName16.Gray))] + [InlineData (0, 128, 0, nameof (ColorName16.Green))] + [InlineData (255, 0, 255, nameof (ColorName16.Magenta))] + [InlineData (255, 0, 0, nameof (ColorName16.Red))] + [InlineData (255, 255, 255, nameof (ColorName16.White))] + [InlineData (255, 255, 0, nameof (ColorName16.Yellow))] + // W3C color names + [InlineData (240, 248, 255, nameof (W3cColor.AliceBlue))] + [InlineData (255, 235, 205, nameof (W3cColor.BlanchedAlmond))] + [InlineData (95, 158, 160, nameof (W3cColor.CadetBlue))] + [InlineData (0, 0, 139, nameof (W3cColor.DarkBlue))] + [InlineData (178, 34, 34, nameof (W3cColor.FireBrick))] + [InlineData (220, 220, 220, nameof (W3cColor.Gainsboro))] + [InlineData (240, 255, 240, nameof (W3cColor.HoneyDew))] + [InlineData (75, 0, 130, nameof (W3cColor.Indigo))] + [InlineData (240, 230, 140, nameof (W3cColor.Khaki))] + [InlineData (230, 230, 250, nameof (W3cColor.Lavender))] + [InlineData (128, 0, 0, nameof (W3cColor.Maroon))] + [InlineData (0, 0, 128, nameof (W3cColor.Navy))] + [InlineData (128, 128, 0, nameof (W3cColor.Olive))] + [InlineData (221, 160, 221, nameof (W3cColor.Plum))] + [InlineData (65, 105, 225, nameof (W3cColor.RoyalBlue))] + [InlineData (192, 192, 192, nameof (W3cColor.Silver))] + [InlineData (255, 99, 71, nameof (W3cColor.Tomato))] + [InlineData (238, 130, 238, nameof (W3cColor.Violet))] + [InlineData (245, 245, 245, nameof (W3cColor.WhiteSmoke))] + [InlineData (154, 205, 50, nameof (W3cColor.YellowGreen))] + public void TryNameColor_ReturnsExpectedColorNames (byte r, byte g, byte b, string expectedName) + { + var expected = (true, expectedName); + + bool actualSuccess = _candidate.TryNameColor(new Color(r, g, b), out string? actualName); + var actual = (actualSuccess, actualName); + + Assert.Equal (expected, actual); + } + + [Theory] + [InlineData (169, 169, 169)] // W3cColor.DarkGr(a|e)y + public void TryNameColor_OmitsBlockedW3cColors (byte r, byte g, byte b) + { + (bool, string?) expected = (false, null); + + bool actualSuccess = _candidate.TryNameColor (new Color (r, g, b), out string? actualName); + var actual = (actualSuccess, actualName); + + Assert.Equal (expected, actual); + } + + [Fact] + public void TryNameColor_NoMatchFails () + { + (bool, string?) expected = (false, null); + + bool actualSuccess = _candidate.TryNameColor (new Color (1, 2, 3), out string? actualName); + var actual = (actualSuccess, actualName); + + Assert.Equal (expected, actual); + } + + [Theory] + // ANSI colors + [InlineData (nameof (ColorName16.Black), 0, 0, 0)] + [InlineData (nameof (ColorName16.Blue), 0, 0, 255)] + [InlineData (nameof (ColorName16.BrightBlue), 59, 120, 255)] + [InlineData (nameof (ColorName16.BrightCyan), 97, 214, 214)] + [InlineData (nameof (ColorName16.BrightGreen), 22, 198, 12)] + [InlineData (nameof (ColorName16.BrightMagenta), 180, 0, 158)] + [InlineData (nameof (ColorName16.BrightRed), 231, 72, 86)] + [InlineData (nameof (ColorName16.BrightYellow), 249, 241, 165)] + [InlineData (nameof (ColorName16.Cyan), 0, 255, 255)] + [InlineData (nameof (ColorName16.DarkGray), 118, 118, 118)] + [InlineData (nameof (ColorName16.Gray), 128, 128, 128)] + [InlineData (nameof (ColorName16.Green), 0, 128, 0)] + [InlineData (nameof (ColorName16.Magenta), 255, 0, 255)] + [InlineData (nameof (ColorName16.Red), 255, 0, 0)] + [InlineData (nameof (ColorName16.White), 255, 255, 255)] + [InlineData (nameof (ColorName16.Yellow), 255, 255, 0)] + // W3C color name => substituted ANSI color + [InlineData (nameof (W3cColor.Fuchsia), 255, 0, 255)] // ANSI Magenta + [InlineData (nameof (W3cColor.DarkGrey), 118, 118, 118)] // ANSI Dark Gray + [InlineData (nameof (W3cColor.Grey), 128, 128, 128)] // ANSI Gray + [InlineData (nameof (W3cColor.Aqua), 0, 255, 255)] // ANSI Cyan + // W3C colors + [InlineData (nameof (W3cColor.AliceBlue), 240, 248, 255)] + [InlineData (nameof (W3cColor.BlanchedAlmond), 255, 235, 205)] + [InlineData (nameof (W3cColor.CadetBlue), 95, 158, 160)] + [InlineData (nameof (W3cColor.DarkBlue), 0, 0, 139)] + [InlineData (nameof (W3cColor.FireBrick), 178, 34, 34)] + [InlineData (nameof (W3cColor.Gainsboro), 220, 220, 220)] + [InlineData (nameof (W3cColor.HoneyDew), 240, 255, 240)] + [InlineData (nameof (W3cColor.Indigo), 75, 0, 130)] + [InlineData (nameof (W3cColor.Khaki), 240, 230, 140)] + [InlineData (nameof (W3cColor.Lavender), 230, 230, 250)] + [InlineData (nameof (W3cColor.Maroon), 128, 0, 0)] + [InlineData (nameof (W3cColor.Navy), 0, 0, 128)] + [InlineData (nameof (W3cColor.Olive), 128, 128, 0)] + [InlineData (nameof (W3cColor.Plum), 221, 160, 221)] + [InlineData (nameof (W3cColor.RoyalBlue), 65, 105, 225)] + [InlineData (nameof (W3cColor.Silver), 192, 192, 192)] + [InlineData (nameof (W3cColor.Tomato), 255, 99, 71)] + [InlineData (nameof (W3cColor.Violet), 238, 130, 238)] + [InlineData (nameof (W3cColor.WhiteSmoke), 245, 245, 245)] + [InlineData (nameof (W3cColor.YellowGreen), 154, 205, 50)] + // Case-insensitive + [InlineData ("BRIGHTBLUE", 59, 120, 255)] + [InlineData ("brightblue", 59, 120, 255)] + [InlineData ("TOMATO", 255, 99, 71)] + [InlineData ("tomato", 255, 99, 71)] + + public void TryParseColor_ResolvesCombinationOfAnsiAndW3cColors (string inputName, byte r, byte g, byte b) + { + var expected = (true, new Color(r, g, b)); + + bool actualSuccess = _candidate.TryParseColor (inputName, out Color actualColor); + var actual = (actualSuccess, actualColor); + + Assert.Equal (expected, actual); + } + + [Theory] + [InlineData ("12", 231, 72, 86)] // ColorName16.BrightRed + [InlineData ("16737095", 255, 99, 71)] // W3cColor.Tomato + public void TryParseColor_ResolvesValidEnumNumber (string inputName, byte r, byte g, byte b) + { + var expected = (true, new Color(r, g, b)); + + bool actualSuccess = _candidate.TryParseColor (inputName, out Color actualColor); + var actual = (actualSuccess, actualColor); + + Assert.Equal (expected, actual); + } + + [Theory] + [InlineData (null)] + [InlineData ("")] + [InlineData ("brightlight")] + public void TryParseColor_FailsOnInvalidColorName (string? invalidName) + { + var expected = (false, default(Color)); + + bool actualSuccess = _candidate.TryParseColor (invalidName, out Color actualColor); + var actual = (actualSuccess, actualColor); + + Assert.Equal (expected, actual); + } + + [Theory] + [InlineData ("-12")] + [InlineData ("-16737095")] + public void TryParseColor_FailsOnInvalidEnumNumber (string invalidName) + { + var expected = (false, default(Color)); + + bool actualSuccess = _candidate.TryParseColor (invalidName, out Color actualColor); + var actual = (actualSuccess, actualColor); + + Assert.Equal (expected, actual); + } +} diff --git a/Tests/UnitTestsParallelizable/Drawing/Color/W3cColorNameResolverTests.cs b/Tests/UnitTestsParallelizable/Drawing/Color/W3cColorNameResolverTests.cs new file mode 100644 index 0000000000..527c0be4ea --- /dev/null +++ b/Tests/UnitTestsParallelizable/Drawing/Color/W3cColorNameResolverTests.cs @@ -0,0 +1,150 @@ +#nullable enable + +namespace Terminal.Gui.DrawingTests; + +public class W3cColorNameResolverTests +{ + private readonly W3cColorNameResolver _candidate = new(); + + [Fact] + public void GetColorNames_NamesAreInAlphabeticalOrder () + { + string[] alphabeticallyOrderedNames = Enum.GetNames ().Order ().ToArray (); + + Assert.Equal (alphabeticallyOrderedNames, _candidate.GetColorNames ()); + } + + [Theory] + [InlineData (nameof (W3cColor.Aqua))] + [InlineData (nameof (W3cColor.Cyan))] + [InlineData (nameof (W3cColor.DarkGray))] + [InlineData (nameof (W3cColor.DarkGrey))] + [InlineData (nameof (W3cColor.DarkSlateGray))] + [InlineData (nameof (W3cColor.DarkSlateGrey))] + [InlineData (nameof (W3cColor.DimGray))] + [InlineData (nameof (W3cColor.DimGrey))] + [InlineData (nameof (W3cColor.Fuchsia))] + [InlineData (nameof (W3cColor.LightGray))] + [InlineData (nameof (W3cColor.LightGrey))] + [InlineData (nameof (W3cColor.LightSlateGray))] + [InlineData (nameof (W3cColor.LightSlateGrey))] + [InlineData (nameof (W3cColor.Magenta))] + [InlineData (nameof (W3cColor.SlateGray))] + [InlineData (nameof (W3cColor.SlateGrey))] + public void GetColorNames_IncludesNamesWithSameValues (string name) + { + string[] names = _candidate.GetColorNames ().ToArray(); + + Assert.True (names.Contains (name), $"W3C color names is missing '{name}'."); + } + + [Theory] + [InlineData (240, 248, 255, nameof (W3cColor.AliceBlue))] + [InlineData (0, 255, 255, nameof (W3cColor.Aqua))] + [InlineData (255, 0, 0, nameof (W3cColor.Red))] + [InlineData (0, 128, 0, nameof (W3cColor.Green))] + [InlineData (0, 0, 255, nameof (W3cColor.Blue))] + [InlineData (0, 255, 0, nameof (W3cColor.Lime))] + [InlineData (0, 0, 0, nameof (W3cColor.Black))] + [InlineData (255, 255, 255, nameof (W3cColor.White))] + [InlineData (154, 205, 50, nameof (W3cColor.YellowGreen))] + public void TryNameColor_ReturnsExpectedColorName (int r, int g, int b, string expectedName) + { + var expected = (true, expectedName); + + Color inputColor = new(r, g, b); + bool actualSuccess = _candidate.TryNameColor (inputColor, out string? actualName); + var actual = (actualSuccess, actualName); + + Assert.Equal (expected, actual); + } + + [Fact] + public void TryNameColor_NoMatchFails () + { + (bool, string?) expected = (false, null); + + bool actualSuccess = _candidate.TryNameColor (new Color (1, 2, 3), out string? actualName); + var actual = (actualSuccess, actualName); + + Assert.Equal (expected, actual); + } + + [Theory] + [InlineData (nameof (W3cColor.AliceBlue), 240, 248, 255)] + [InlineData (nameof (W3cColor.BlanchedAlmond), 255, 235, 205)] + [InlineData (nameof (W3cColor.CadetBlue), 95, 158, 160)] + [InlineData (nameof (W3cColor.DarkBlue), 0, 0, 139)] + [InlineData (nameof (W3cColor.FireBrick), 178, 34, 34)] + [InlineData (nameof (W3cColor.Gainsboro), 220, 220, 220)] + [InlineData (nameof (W3cColor.HoneyDew), 240, 255, 240)] + [InlineData (nameof (W3cColor.Indigo), 75, 0, 130)] + [InlineData (nameof (W3cColor.Khaki), 240, 230, 140)] + [InlineData (nameof (W3cColor.Lavender), 230, 230, 250)] + [InlineData (nameof (W3cColor.Maroon), 128, 0, 0)] + [InlineData (nameof (W3cColor.Navy), 0, 0, 128)] + [InlineData (nameof (W3cColor.Olive), 128, 128, 0)] + [InlineData (nameof (W3cColor.Plum), 221, 160, 221)] + [InlineData (nameof (W3cColor.RoyalBlue), 65, 105, 225)] + [InlineData (nameof (W3cColor.Silver), 192, 192, 192)] + [InlineData (nameof (W3cColor.Tomato), 255, 99, 71)] + [InlineData (nameof (W3cColor.Violet), 238, 130, 238)] + [InlineData (nameof (W3cColor.WhiteSmoke), 245, 245, 245)] + [InlineData (nameof (W3cColor.YellowGreen), 154, 205, 50)] + // Aliases also work + [InlineData (nameof (W3cColor.Aqua), 0, 255, 255)] + [InlineData (nameof (W3cColor.Cyan), 0, 255, 255)] + [InlineData (nameof (W3cColor.DarkGray), 169, 169, 169)] + [InlineData (nameof (W3cColor.DarkGrey), 169, 169, 169)] + // Case-insensitive + [InlineData ("Red", 255, 0, 0)] + [InlineData ("red", 255, 0, 0)] + [InlineData ("RED", 255, 0, 0)] + public void TryParseColor_ReturnsExpectedColor (string inputName, int r, int g, int b) + { + var expected = (true, new Color(r, g, b)); + + bool actualSuccess = _candidate.TryParseColor (inputName, out Color actualColor); + var actual = (actualSuccess, actualColor); + + Assert.Equal (expected, actual); + } + + [Theory] + [InlineData ("16737095", 255, 99, 71)] // W3cColor.Tomato + public void TryParseColor_ResolvesValidEnumNumber (string inputName, byte r, byte g, byte b) + { + var expected = (true, new Color(r, g, b)); + + bool actualSuccess = _candidate.TryParseColor (inputName, out Color actualColor); + var actual = (actualSuccess, actualColor); + + Assert.Equal (expected, actual); + } + + [Theory] + [InlineData (null)] + [InlineData ("")] + [InlineData ("brightlight")] + public void TryParseColor_FailsOnInvalidColorName (string? invalidName) + { + var expected = (false, default(Color)); + + bool actualSuccess = _candidate.TryParseColor (invalidName, out Color actualColor); + var actual = (actualSuccess, actualColor); + + Assert.Equal (expected, actual); + } + + [Theory] + [InlineData ("-16737095")] + public void TryParseColor_FailsOnInvalidEnumNumber (string invalidName) + { + var expected = (false, default(Color)); + + bool actualSuccess = _candidate.TryParseColor (invalidName, out Color actualColor); + var actual = (actualSuccess, actualColor); + + Assert.Equal (expected, actual); + } +} From cabe4115d1043e0c566b8fd59f5cda8d8e89b550 Mon Sep 17 00:00:00 2001 From: Tig Date: Sat, 29 Mar 2025 11:30:52 -0600 Subject: [PATCH 03/28] Fixes #3691 - Adds `ViewArrangement.Popover` (#3852) * Added Applicaton.Popover. Refactored FindDeepestView * Popover prototype * Testing highlight * Fixed click outside issue * Fixed DialogTests * Fixed click outside issue (agbain) * Enabled mouse wheel in Bar * Enabled mouse wheel in Bar * Progress. Broke arrangement * Added popover tests. Fixed a bunch more CM issues related ot unreliable unit tests. Updated config.json to include Glyphs. * Can't set ForceDriver to empty in Resources/config.json. * added BUGBUG * Made Position/ScreenPosition clear * Added View.IsInHierarchy tests * Added Contextmenuv2 scenario. * Implemented CM2 in TextView * Removed unneeded CM stuff from testhelpers * Shortcut API docs * Fixed keybinding unit tests * Fixed mouse handling * Fighting with CM related unit test failures * Unit tests pass. I think. * Shortcut code cleanup * TextView uses new CM2 * Starting on OnSelect etc... * Starting on OnSelect etc... * Fixed ContextMenuv2 * ContextMenu is working again. * Ugh. ANd fixed button api docs * Fixed DrawHorizontalShadowTransparent (vertical was already fixed). * Made Scenarios compatible with #nullable enable * Undid some keybinding stuff * Fixed stuff * Sped up unit tests * Sped up unit tests 2 * Sped up unit tests 3 * Messing with menus * merged latest v2_develop * Added more Popover unit tests * Added more Popover unit tests2 * Fixed positioning bug * Fixed mouse bug * Fixed Bar draw issue * WIP * merge v2_develop * CM2 sorta works * Enabled Bar subclasses to have IDesignable * Added ViewportSettings.Transparent * Region -> nullable enable * Added ViewportSettigs Editor * merged v2_develop part 2 * merged v2_develop part 3 * WIP: GetViewsUnderMouse * WIP: More GetViewsUnderMouse work * Bars works again * Added unit tests * CM now works * MenuItemv2 POC * SubMenu POC * CommandNotBound * More POC * Optimize Margin to not defer draw if there's no shadow * Logger cleanup * Reverted Generic * Cascading mostly working * fixed layout bug * API docs * API docs * Fixed cascade * Events basically work * code cleanup * Fixed IsDefault bug; * Enabled hotkey support * Made context-menu-like * Improved usability * Refactored ApplicationPopover again * Cleanup * Menuv2 POC basically complete * Code Cleanup * Made menu API simpler * Fixed Strings bugs * Got old ContextMenu scenario mostly working * ContextMenu scenario now works * ContextMenu fixes * ContextMenu fixes * Tons of menu cleanup * ContextMenu works in TextView * Fixed unit tes * Added unit tests * Fixed tests * code cleanup * More code cleanup * Deep dive * scenario * typos * Demo colorpicker in a Menu * Added Region tests proving Region is broken in some Union cases * fixed v2win/net --- .../Application/Application.Initialization.cs | 9 +- .../Application/Application.Keyboard.cs | 88 ++- Terminal.Gui/Application/Application.Mouse.cs | 16 + .../Application/Application.Popover.cs | 9 + Terminal.Gui/Application/Application.Run.cs | 17 +- Terminal.Gui/Application/Application.cs | 11 +- .../Application/ApplicationNavigation.cs | 4 + .../Application/ApplicationPopover.cs | 160 +++++ Terminal.Gui/Application/IPopover.cs | 10 + Terminal.Gui/Application/PopoverBaseImpl.cs | 78 +++ .../AnsiResponseParser/AnsiMouseParser.cs | 2 +- Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs | 2 +- .../ConsoleDrivers/V2/ApplicationV2.cs | 1 + .../ConsoleDrivers/V2/InputProcessor.cs | 2 +- Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs | 10 +- .../Drawing/Color/ColorScheme.Colors.cs | 11 +- Terminal.Gui/Input/Command.cs | 8 + Terminal.Gui/Input/CommandContext.cs | 11 +- Terminal.Gui/Input/ICommandContext.cs | 6 + Terminal.Gui/Input/IInputBinding.cs | 6 + Terminal.Gui/Input/InputBindings.cs | 2 +- Terminal.Gui/Input/Keyboard/KeyBinding.cs | 8 +- Terminal.Gui/Input/Mouse/MouseBinding.cs | 3 + Terminal.Gui/Resources/GlobalResources.cs | 2 +- .../Resources/ResourceManagerWrapper.cs | 6 +- Terminal.Gui/Resources/Strings.Designer.cs | 162 +++++ Terminal.Gui/Resources/Strings.fr-FR.resx | 54 ++ Terminal.Gui/Resources/Strings.ja-JP.resx | 54 ++ Terminal.Gui/Resources/Strings.pt-PT.resx | 54 ++ Terminal.Gui/Resources/Strings.resx | 54 ++ Terminal.Gui/Resources/Strings.zh-Hans.resx | 54 ++ .../View/SuperViewChangedEventArgs.cs | 5 +- Terminal.Gui/View/View.Adornments.cs | 8 +- Terminal.Gui/View/View.Command.cs | 93 ++- Terminal.Gui/View/View.Diagnostics.cs | 29 - Terminal.Gui/View/View.Drawing.cs | 40 +- Terminal.Gui/View/View.Keyboard.cs | 44 +- Terminal.Gui/View/View.Layout.cs | 29 +- Terminal.Gui/View/View.Mouse.cs | 21 +- Terminal.Gui/View/View.Navigation.cs | 33 ++ Terminal.Gui/View/View.cs | 2 + Terminal.Gui/View/ViewArrangement.cs | 2 +- Terminal.Gui/View/ViewDiagnosticFlags.cs | 31 + Terminal.Gui/Views/Bar.cs | 20 +- Terminal.Gui/Views/Button.cs | 2 +- Terminal.Gui/Views/ComboBox.cs | 14 +- Terminal.Gui/Views/Menu/ContextMenuv2.cs | 104 ++++ Terminal.Gui/Views/Menu/Menu.cs | 3 +- Terminal.Gui/Views/Menu/MenuBarItemv2.cs | 98 +++ Terminal.Gui/Views/Menu/MenuBarv2.cs | 342 +++++++++++ Terminal.Gui/Views/Menu/MenuItemv2.cs | 200 +++++++ Terminal.Gui/Views/Menu/Menuv2.cs | 172 ++++++ Terminal.Gui/Views/Menu/PopoverMenu.cs | 532 +++++++++++++++++ Terminal.Gui/Views/MenuBarv2.cs | 51 -- Terminal.Gui/Views/Menuv2.cs | 98 --- Terminal.Gui/Views/MessageBox.cs | 18 +- Terminal.Gui/Views/ScrollBar/ScrollSlider.cs | 2 +- Terminal.Gui/Views/Shortcut.cs | 163 +++-- Terminal.Gui/Views/Slider.cs | 2 +- Terminal.Gui/Views/TextField.cs | 143 +++-- Terminal.Gui/Views/TextView.cs | 123 ++-- .../Application/ApplicationPopoverTests.cs | 444 ++++++++++++++ .../UnitTests/Application/ApplicationTests.cs | 3 +- .../Configuration/ConfigurationMangerTests.cs | 2 +- .../Resources/ResourceManagerTests.cs | 5 +- .../View/Mouse/GetViewsUnderMouseTests.cs | 53 ++ Tests/UnitTests/Views/ContextMenuTests.cs | 96 +-- Tests/UnitTests/Views/TextFieldTests.cs | 2 +- Tests/UnitTests/Views/TextViewTests.cs | 2 +- .../Application/ApplicationPopoverTests.cs | 163 +++++ .../Drawing/DrawContextTests.cs | 20 + .../Drawing/Region/RegionTests.cs | 40 ++ .../View/ViewCommandTests.cs | 63 +- UICatalog/Scenarios/Arrangement.cs | 28 + UICatalog/Scenarios/Bars.cs | 18 +- UICatalog/Scenarios/ColorPicker.cs | 2 +- UICatalog/Scenarios/ContextMenus.cs | 360 +++++------- UICatalog/Scenarios/Editor.cs | 10 +- UICatalog/Scenarios/Generic.cs | 16 +- UICatalog/Scenarios/MenusV2.cs | 556 ++++++++++++++++++ UICatalog/Scenarios/Snake.cs | 2 +- UICatalog/Scenarios/Transparent.cs | 22 +- UICatalog/Scenarios/ViewExperiments.cs | 46 ++ UICatalog/UICatalog.cs | 21 +- docfx/docs/Popovers.md | 18 + docfx/docs/index.md | 1 + docfx/docs/logging.md | 16 +- docfx/docs/toc.yml | 2 + docfx/images/UICatalog_Logging.png | Bin 0 -> 53046 bytes 89 files changed, 4438 insertions(+), 911 deletions(-) create mode 100644 Terminal.Gui/Application/Application.Popover.cs create mode 100644 Terminal.Gui/Application/ApplicationPopover.cs create mode 100644 Terminal.Gui/Application/IPopover.cs create mode 100644 Terminal.Gui/Application/PopoverBaseImpl.cs create mode 100644 Terminal.Gui/View/ViewDiagnosticFlags.cs create mode 100644 Terminal.Gui/Views/Menu/ContextMenuv2.cs create mode 100644 Terminal.Gui/Views/Menu/MenuBarItemv2.cs create mode 100644 Terminal.Gui/Views/Menu/MenuBarv2.cs create mode 100644 Terminal.Gui/Views/Menu/MenuItemv2.cs create mode 100644 Terminal.Gui/Views/Menu/Menuv2.cs create mode 100644 Terminal.Gui/Views/Menu/PopoverMenu.cs delete mode 100644 Terminal.Gui/Views/MenuBarv2.cs delete mode 100644 Terminal.Gui/Views/Menuv2.cs create mode 100644 Tests/UnitTests/Application/ApplicationPopoverTests.cs create mode 100644 Tests/UnitTestsParallelizable/Application/ApplicationPopoverTests.cs create mode 100644 Tests/UnitTestsParallelizable/Drawing/DrawContextTests.cs create mode 100644 UICatalog/Scenarios/MenusV2.cs create mode 100644 docfx/docs/Popovers.md create mode 100644 docfx/images/UICatalog_Logging.png diff --git a/Terminal.Gui/Application/Application.Initialization.cs b/Terminal.Gui/Application/Application.Initialization.cs index 923b1534d5..7093b87025 100644 --- a/Terminal.Gui/Application/Application.Initialization.cs +++ b/Terminal.Gui/Application/Application.Initialization.cs @@ -83,6 +83,7 @@ internal static void InternalInit ( } Navigation = new (); + Popover = new (); // For UnitTests if (driver is { }) @@ -162,6 +163,12 @@ internal static void InternalInit ( SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext ()); + // TODO: This is probably not needed + if (Popover.GetActivePopover () is View popover) + { + popover.Visible = false; + } + MainThreadId = Thread.CurrentThread.ManagedThreadId; bool init = Initialized = true; InitializedChanged?.Invoke (null, new (init)); @@ -265,6 +272,6 @@ internal static void UnsubscribeDriverEvents () /// internal static void OnInitializedChanged (object sender, EventArgs e) { - Application.InitializedChanged?.Invoke (sender,e); + Application.InitializedChanged?.Invoke (sender, e); } } diff --git a/Terminal.Gui/Application/Application.Keyboard.cs b/Terminal.Gui/Application/Application.Keyboard.cs index 873dc0af0f..eda91faf62 100644 --- a/Terminal.Gui/Application/Application.Keyboard.cs +++ b/Terminal.Gui/Application/Application.Keyboard.cs @@ -13,6 +13,7 @@ public static partial class Application // Keyboard handling /// if the key was handled. public static bool RaiseKeyDownEvent (Key key) { + // TODO: This should match standard event patterns KeyDown?.Invoke (null, key); if (key.Handled) @@ -20,6 +21,11 @@ public static bool RaiseKeyDownEvent (Key key) return true; } + if (Popover?.DispatchKeyDown (key) is true) + { + return true; + } + if (Top is null) { foreach (Toplevel topLevel in TopLevels.ToList ()) @@ -43,6 +49,27 @@ public static bool RaiseKeyDownEvent (Key key) } } + bool? commandHandled = InvokeCommandsBoundToKey (key); + if(commandHandled is true) + { + return true; + } + + return false; + } + + /// + /// Invokes any commands bound at the Application-level to . + /// + /// + /// + /// if no command was found; input processing should continue. + /// if the command was invoked and was not handled (or cancelled); input processing should continue. + /// if the command was invoked the command was handled (or cancelled); input processing should stop. + /// + public static bool? InvokeCommandsBoundToKey (Key key) + { + bool? handled = null; // Invoke any Application-scoped KeyBindings. // The first view that handles the key will stop the loop. // foreach (KeyValuePair binding in KeyBindings.GetBindings (key)) @@ -52,22 +79,17 @@ public static bool RaiseKeyDownEvent (Key key) { if (!binding.Target.Enabled) { - return false; + return null; } - bool? handled = binding.Target?.InvokeCommands (binding.Commands, binding); - - if (handled != null && (bool)handled) - { - return true; - } + handled = binding.Target?.InvokeCommands (binding.Commands, binding); } else { // BUGBUG: this seems unneeded. if (!KeyBindings.TryGet (key, out KeyBinding keybinding)) { - return false; + return null; } bool? toReturn = null; @@ -77,30 +99,42 @@ public static bool RaiseKeyDownEvent (Key key) toReturn = InvokeCommand (command, key, keybinding); } - return toReturn ?? true; + handled = toReturn ?? true; } } - return false; + return handled; + } - static bool? InvokeCommand (Command command, Key key, KeyBinding binding) + /// + /// Invokes an Application-bound commmand. + /// + /// The Command to invoke + /// The Application-bound Key that was pressed. + /// Describes the binding. + /// + /// if no command was found; input processing should continue. + /// if the command was invoked and was not handled (or cancelled); input processing should continue. + /// if the command was invoked the command was handled (or cancelled); input processing should stop. + /// + /// + public static bool? InvokeCommand (Command command, Key key, KeyBinding binding) + { + if (!_commandImplementations!.ContainsKey (command)) { - if (!_commandImplementations!.ContainsKey (command)) - { - throw new NotSupportedException ( - @$"A KeyBinding was set up for the command {command} ({key}) but that command is not supported by Application." - ); - } - - if (_commandImplementations.TryGetValue (command, out View.CommandImplementation? implementation)) - { - CommandContext context = new (command, binding); // Create the context here + throw new NotSupportedException ( + @$"A KeyBinding was set up for the command {command} ({key}) but that command is not supported by Application." + ); + } - return implementation (context); - } + if (_commandImplementations.TryGetValue (command, out View.CommandImplementation? implementation)) + { + CommandContext context = new (command, null, binding); // Create the context here - return false; + return implementation (context); } + + return null; } /// @@ -167,7 +201,7 @@ internal static void AddKeyBindings () { _commandImplementations.Clear (); - // Things this view knows how to do + // Things Application knows how to do AddCommand ( Command.Quit, static () => @@ -213,7 +247,7 @@ internal static void AddKeyBindings () ); AddCommand ( - Command.Edit, + Command.Arrange, static () => { View? viewToArrange = Navigation?.GetFocused (); @@ -249,7 +283,7 @@ internal static void AddKeyBindings () KeyBindings.Add (PrevTabKey, Command.PreviousTabStop); KeyBindings.Add (NextTabGroupKey, Command.NextTabGroup); KeyBindings.Add (PrevTabGroupKey, Command.PreviousTabGroup); - KeyBindings.Add (ArrangeKey, Command.Edit); + KeyBindings.Add (ArrangeKey, Command.Arrange); KeyBindings.Add (Key.CursorRight, Command.NextTabStop); KeyBindings.Add (Key.CursorDown, Command.NextTabStop); diff --git a/Terminal.Gui/Application/Application.Mouse.cs b/Terminal.Gui/Application/Application.Mouse.cs index 01bdcc1a38..72cffe368b 100644 --- a/Terminal.Gui/Application/Application.Mouse.cs +++ b/Terminal.Gui/Application/Application.Mouse.cs @@ -1,5 +1,6 @@ #nullable enable using System.ComponentModel; +using System.Diagnostics; namespace Terminal.Gui; @@ -168,6 +169,20 @@ internal static void RaiseMouseEvent (MouseEventArgs mouseEvent) return; } + // Dismiss the Popover if the user presses mouse outside of it + if (mouseEvent.IsPressed + && Popover?.GetActivePopover () as View is { Visible: true } visiblePopover + && View.IsInHierarchy (visiblePopover, deepestViewUnderMouse, includeAdornments: true) is false) + { + + visiblePopover.Visible = false; + + // Recurse once so the event can be handled below the popover + RaiseMouseEvent (mouseEvent); + + return; + } + if (HandleMouseGrab (deepestViewUnderMouse, mouseEvent)) { return; @@ -216,6 +231,7 @@ internal static void RaiseMouseEvent (MouseEventArgs mouseEvent) else { // The mouse was outside any View's Viewport. + //Debug.Fail ("this should not happen."); // Debug.Fail ("This should never happen. If it does please file an Issue!!"); diff --git a/Terminal.Gui/Application/Application.Popover.cs b/Terminal.Gui/Application/Application.Popover.cs new file mode 100644 index 0000000000..104994a0e1 --- /dev/null +++ b/Terminal.Gui/Application/Application.Popover.cs @@ -0,0 +1,9 @@ +#nullable enable + +namespace Terminal.Gui; + +public static partial class Application // Popover handling +{ + /// Gets the Application manager. + public static ApplicationPopover? Popover { get; internal set; } +} \ No newline at end of file diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs index 8fbea90342..4412f5d233 100644 --- a/Terminal.Gui/Application/Application.Run.cs +++ b/Terminal.Gui/Application/Application.Run.cs @@ -337,7 +337,7 @@ public static Toplevel Run (Func? errorHandler = null, IConsole [RequiresUnreferencedCode ("AOT")] [RequiresDynamicCode ("AOT")] public static T Run (Func? errorHandler = null, IConsoleDriver? driver = null) - where T : Toplevel, new () + where T : Toplevel, new() { return ApplicationImpl.Instance.Run (errorHandler, driver); } @@ -426,7 +426,16 @@ public static T Run (Func? errorHandler = null, IConsoleDriv internal static void LayoutAndDrawImpl (bool forceDraw = false) { - bool neededLayout = View.Layout (TopLevels.Reverse (), Screen.Size); + List tops = [..TopLevels]; + + if (Popover?.GetActivePopover () as View is { Visible: true } visiblePopover) + { + visiblePopover.SetNeedsDraw (); + visiblePopover.SetNeedsLayout (); + tops.Insert (0, visiblePopover); + } + + bool neededLayout = View.Layout (tops.ToArray ().Reverse (), Screen.Size); if (ClearScreenNextIteration) { @@ -440,7 +449,7 @@ internal static void LayoutAndDrawImpl (bool forceDraw = false) } View.SetClipToScreen (); - View.Draw (TopLevels, neededLayout || forceDraw); + View.Draw (tops, neededLayout || forceDraw); View.SetClipToScreen (); Driver?.Refresh (); } @@ -555,6 +564,8 @@ public static void End (RunState runState) { ArgumentNullException.ThrowIfNull (runState); + Popover?.HidePopover (Popover?.GetActivePopover ()); + runState.Toplevel.OnUnloaded (); // End the RunState.Toplevel diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs index 9ea1b45040..5d01106ae4 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -103,6 +103,7 @@ internal static List GetAvailableCulturesFromEmbeddedResources () .ToList (); } + // BUGBUG: This does not return en-US even though it's supported by default internal static List GetSupportedCultures () { CultureInfo [] cultures = CultureInfo.GetCultures (CultureTypes.AllCultures); @@ -148,6 +149,12 @@ internal static void ResetState (bool ignoreDisposed = false) t!.Running = false; } + if (Popover?.GetActivePopover () is View popover) + { + popover.Visible = false; + } + Popover = null; + TopLevels.Clear (); #if DEBUG_IDISPOSABLE @@ -197,7 +204,9 @@ internal static void ResetState (bool ignoreDisposed = false) Initialized = false; // Mouse - _lastMousePosition = null; + // Do not clear _lastMousePosition; Popover's require it to stay set with + // last mouse pos. + //_lastMousePosition = null; _cachedViewsUnderMouse.Clear (); WantContinuousButtonPressedView = null; MouseEvent = null; diff --git a/Terminal.Gui/Application/ApplicationNavigation.cs b/Terminal.Gui/Application/ApplicationNavigation.cs index f351b515d6..985d5124e0 100644 --- a/Terminal.Gui/Application/ApplicationNavigation.cs +++ b/Terminal.Gui/Application/ApplicationNavigation.cs @@ -104,6 +104,10 @@ internal void SetFocused (View? value) /// public bool AdvanceFocus (NavigationDirection direction, TabBehavior? behavior) { + if (Application.Popover?.GetActivePopover () as View is { Visible: true } visiblePopover) + { + return visiblePopover.AdvanceFocus (direction, behavior); + } return Application.Top is { } && Application.Top.AdvanceFocus (direction, behavior); } } diff --git a/Terminal.Gui/Application/ApplicationPopover.cs b/Terminal.Gui/Application/ApplicationPopover.cs new file mode 100644 index 0000000000..9c0b944623 --- /dev/null +++ b/Terminal.Gui/Application/ApplicationPopover.cs @@ -0,0 +1,160 @@ +#nullable enable + +using System.Diagnostics; + +namespace Terminal.Gui; + +/// +/// Helper class for support of views for . Held by +/// +public class ApplicationPopover +{ + /// + /// Initializes a new instance of the class. + /// + public ApplicationPopover () { } + + private readonly List _popovers = []; + + /// + public IReadOnlyCollection Popovers => _popovers.AsReadOnly (); + + /// + /// Registers with the application. + /// This enables the popover to receive keyboard events even when when it is not active. + /// + /// + public void Register (IPopover? popover) + { + if (popover is { } && !_popovers.Contains (popover)) + { + _popovers.Add (popover); + + } + } + + /// + /// De-registers with the application. Use this to remove the popover and it's + /// keyboard bindings from the application. + /// + /// + /// + public bool DeRegister (IPopover? popover) + { + if (popover is { } && _popovers.Contains (popover)) + { + if (GetActivePopover () == popover) + { + _activePopover = null; + } + + _popovers.Remove (popover); + + return true; + } + + return false; + } + + private IPopover? _activePopover; + + /// + /// Gets the active popover, if any. + /// + /// + public IPopover? GetActivePopover () { return _activePopover; } + + /// + /// Shows . IPopover implementations should use OnVisibleChnaged/VisibleChanged to be + /// notified when the user has done something to cause the popover to be hidden. + /// + /// + /// + /// Note, this API calls . To disable the popover from processing keyboard events, + /// either call to + /// remove the popover from the application or set to . + /// + /// + /// + public void ShowPopover (IPopover? popover) + { + // If there's an existing popover, hide it. + if (_activePopover is View popoverView) + { + popoverView.Visible = false; + _activePopover = null; + } + + if (popover is View newPopover) + { + Register (popover); + + if (!newPopover.IsInitialized) + { + newPopover.BeginInit (); + newPopover.EndInit (); + } + + _activePopover = newPopover as IPopover; + newPopover.Enabled = true; + newPopover.Visible = true; + } + } + + /// + /// Causes the specified popover to be hidden. + /// If the popover is dervied from , this is the same as setting to . + /// + /// + public void HidePopover (IPopover? popover) + { + // If there's an existing popover, hide it. + if (_activePopover is View popoverView && popoverView == popover) + { + popoverView.Visible = false; + _activePopover = null; + Application.Top?.SetNeedsDraw (); + } + } + + + /// + /// Called when the user presses a key. Dispatches the key to the active popover, if any, + /// otherwise to the popovers in the order they were registered. Inactive popovers only get hotkeys. + /// + /// + /// + internal bool DispatchKeyDown (Key key) + { + // Do active first - Active gets all key down events. + if (GetActivePopover () as View is { Visible: true } visiblePopover) + { + if (visiblePopover.NewKeyDownEvent (key)) + { + return true; + } + } + + // If the active popover didn't handle the key, try the inactive ones. + // Inactive only get hotkeys + bool? hotKeyHandled = null; + + foreach (IPopover popover in _popovers) + { + if (GetActivePopover () == popover || popover is not View popoverView) + { + continue; + } + + // hotKeyHandled = popoverView.InvokeCommandsBoundToHotKey (key); + hotKeyHandled = popoverView.NewKeyDownEvent (key); + + if (hotKeyHandled is true) + { + return true; + } + } + + return hotKeyHandled is true; + } +} diff --git a/Terminal.Gui/Application/IPopover.cs b/Terminal.Gui/Application/IPopover.cs new file mode 100644 index 0000000000..d168241fe5 --- /dev/null +++ b/Terminal.Gui/Application/IPopover.cs @@ -0,0 +1,10 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Interface identifying a View as being capable of being a Popover. +/// +public interface IPopover +{ + +} diff --git a/Terminal.Gui/Application/PopoverBaseImpl.cs b/Terminal.Gui/Application/PopoverBaseImpl.cs new file mode 100644 index 0000000000..fce7d78660 --- /dev/null +++ b/Terminal.Gui/Application/PopoverBaseImpl.cs @@ -0,0 +1,78 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Abstract base class for Popover Views. +/// +/// +/// +/// To show a Popover, use . To hide a popover, +/// call with set to . +/// +/// +/// If the user clicks anywhere not occulded by a SubView of the Popover, presses , +/// or causes another popover to show, the Popover will be hidden. +/// +/// + +public abstract class PopoverBaseImpl : View, IPopover +{ + /// + /// + /// + protected PopoverBaseImpl () + { + Id = "popoverBaseImpl"; + CanFocus = true; + Width = Dim.Fill (); + Height = Dim.Fill (); + ViewportSettings = ViewportSettings.Transparent | ViewportSettings.TransparentMouse; + + //// TODO: Add a diagnostic setting for this? + TextFormatter.VerticalAlignment = Alignment.End; + TextFormatter.Alignment = Alignment.End; + base.Text = "popover"; + + AddCommand (Command.Quit, Quit); + KeyBindings.Add (Application.QuitKey, Command.Quit); + + return; + + bool? Quit (ICommandContext? ctx) + { + if (!Visible) + { + return null; + } + + Visible = false; + + return true; + } + } + + /// + protected override bool OnVisibleChanging () + { + bool ret = base.OnVisibleChanging (); + if (!ret & !Visible) + { + // Whenvver visible is changing to true, we need to resize; + // it's our only chance because we don't get laid out until we're visible + Layout (Application.Screen.Size); + } + + return ret; + } + + // TODO: Pretty sure this is not needed. set_Visible SetFocus already + ///// + //protected override void OnVisibleChanged () + //{ + // base.OnVisibleChanged (); + // if (Visible) + // { + // //SetFocus (); + // } + //} +} diff --git a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs index 83c6c41817..76d141c277 100644 --- a/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs +++ b/Terminal.Gui/ConsoleDrivers/AnsiResponseParser/AnsiMouseParser.cs @@ -52,7 +52,7 @@ public bool IsMouse (string? cur) Flags = GetFlags (buttonCode, terminator) }; - Logging.Trace ($"{nameof (AnsiMouseParser)} handled as {input} mouse {m.Flags} at {m.Position}"); + //Logging.Trace ($"{nameof (AnsiMouseParser)} handled as {input} mouse {m.Flags} at {m.Position}"); return m; } diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs index f2a08ee85c..8d418dddbb 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs @@ -682,7 +682,7 @@ public void OnMouseEvent (MouseEventArgs a) public void OnKeyUp (Key a) { KeyUp?.Invoke (this, a); } // TODO: Remove this API - it was needed when we didn't have a reliable way to simulate key presses. - // TODO: We now do: Applicaiton.RaiseKeyDown and Application.RaiseKeyUp + // TODO: We now do: Application.RaiseKeyDown and Application.RaiseKeyUp /// Simulates a key press. /// The key character. /// The key. diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index 6ccf8a54b5..9baeba301f 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -64,6 +64,7 @@ public override void Init (IConsoleDriver? driver = null, string? driverName = n } Application.Navigation = new (); + Application.Popover = new (); Application.AddKeyBindings (); diff --git a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs index e7b7b8d2cc..e870fd4e9f 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/InputProcessor.cs @@ -79,7 +79,7 @@ public void OnMouseEvent (MouseEventArgs a) foreach (MouseEventArgs e in _mouseInterpreter.Process (a)) { - Logging.Trace ($"Mouse Interpreter raising {e.Flags}"); + // Logging.Trace ($"Mouse Interpreter raising {e.Flags}"); // Pass on MouseEvent?.Invoke (this, e); diff --git a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs index 0b00165e1f..3021bf2974 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/MainLoop.cs @@ -122,7 +122,8 @@ internal void IterationImpl () if (Application.Top != null) { - bool needsDrawOrLayout = AnySubViewsNeedDrawn (Application.Top); + bool needsDrawOrLayout = AnySubViewsNeedDrawn (Application.Popover?.GetActivePopover () as View) + || AnySubViewsNeedDrawn (Application.Top); bool sizeChanged = WindowSizeMonitor.Poll (); @@ -174,8 +175,13 @@ private void SetCursor () } } - private bool AnySubViewsNeedDrawn (View v) + private bool AnySubViewsNeedDrawn (View? v) { + if (v is null) + { + return false; + } + if (v.NeedsDraw || v.NeedsLayout) { Logging.Trace ($"{v.GetType ().Name} triggered redraw (NeedsDraw={v.NeedsDraw} NeedsLayout={v.NeedsLayout}) "); diff --git a/Terminal.Gui/Drawing/Color/ColorScheme.Colors.cs b/Terminal.Gui/Drawing/Color/ColorScheme.Colors.cs index 96316ac431..5b8b130fdf 100644 --- a/Terminal.Gui/Drawing/Color/ColorScheme.Colors.cs +++ b/Terminal.Gui/Drawing/Color/ColorScheme.Colors.cs @@ -185,11 +185,7 @@ public bool ContainsKey (string key) } } - /// - /// Copies the elements of the to an array, starting at a particular array index. - /// - /// The one-dimensional array that is the destination of the elements copied from . - /// The zero-based index in array at which copying begins. + /// public void CopyTo (KeyValuePair [] array, int arrayIndex) { lock (_lock) @@ -198,10 +194,7 @@ public void CopyTo (KeyValuePair [] array, int arrayIndex) } } - /// - /// Returns an enumerator that iterates through the . - /// - /// An enumerator for the . + /// public IEnumerator> GetEnumerator () { lock (_lock) diff --git a/Terminal.Gui/Input/Command.cs b/Terminal.Gui/Input/Command.cs index 0d7be7ea80..8541142f29 100644 --- a/Terminal.Gui/Input/Command.cs +++ b/Terminal.Gui/Input/Command.cs @@ -14,6 +14,11 @@ namespace Terminal.Gui; /// public enum Command { + /// + /// Indicates the command is not bound or invalid. Will call . + /// + NotBound = 0, + #region Base View Commands /// @@ -270,6 +275,9 @@ public enum Command /// Tabs back to the previous item. BackTab, + /// Enables arrange mode. + Arrange, + #endregion #region Action Commands diff --git a/Terminal.Gui/Input/CommandContext.cs b/Terminal.Gui/Input/CommandContext.cs index bf120996bf..282e2ed4c0 100644 --- a/Terminal.Gui/Input/CommandContext.cs +++ b/Terminal.Gui/Input/CommandContext.cs @@ -1,28 +1,33 @@ #nullable enable namespace Terminal.Gui; -#pragma warning disable CS1574 // XML comment has cref attribute that could not be resolved +#pragma warning disable CS1574, CS0419 // XML comment has cref attribute that could not be resolved /// /// Provides context for a invocation. /// /// . -#pragma warning restore CS1574 // XML comment has cref attribute that could not be resolved +#pragma warning restore CS1574, CS0419 // XML comment has cref attribute that could not be resolved public record struct CommandContext : ICommandContext { /// /// Initializes a new instance with the specified , /// /// + /// /// - public CommandContext (Command command, TBinding? binding) + public CommandContext (Command command, View? source, TBinding? binding) { Command = command; Binding = binding; + Source = source; } /// public Command Command { get; set; } + /// + public View? Source { get; set; } + /// /// The keyboard or mouse minding that was used to invoke the , if any. /// diff --git a/Terminal.Gui/Input/ICommandContext.cs b/Terminal.Gui/Input/ICommandContext.cs index 644029ca28..a7407d7879 100644 --- a/Terminal.Gui/Input/ICommandContext.cs +++ b/Terminal.Gui/Input/ICommandContext.cs @@ -15,4 +15,10 @@ public interface ICommandContext /// The that is being invoked. /// public Command Command { get; set; } + + /// + /// The View that was the source of the command invocation, if any. + /// (e.g. the view the user clicked on or the view that had focus when a key was pressed). + /// + public View? Source { get; set; } } diff --git a/Terminal.Gui/Input/IInputBinding.cs b/Terminal.Gui/Input/IInputBinding.cs index 2ce2bec8bc..eff8353479 100644 --- a/Terminal.Gui/Input/IInputBinding.cs +++ b/Terminal.Gui/Input/IInputBinding.cs @@ -10,4 +10,10 @@ public interface IInputBinding /// Gets or sets the commands this input binding will invoke. /// Command [] Commands { get; set; } + + /// + /// Arbitrary context that can be associated with this input binding. + /// + public object? Data { get; set; } + } diff --git a/Terminal.Gui/Input/InputBindings.cs b/Terminal.Gui/Input/InputBindings.cs index de2578887b..3535570237 100644 --- a/Terminal.Gui/Input/InputBindings.cs +++ b/Terminal.Gui/Input/InputBindings.cs @@ -162,7 +162,7 @@ public Command [] GetCommands (TEvent eventArgs) /// The first matching bound to the set of commands specified by /// . if the set of caommands was not found. /// - public TEvent GetFirstFromCommands (params Command [] commands) { return _bindings.FirstOrDefault (a => a.Value.Commands.SequenceEqual (commands)).Key; } + public TEvent? GetFirstFromCommands (params Command [] commands) { return _bindings.FirstOrDefault (a => a.Value.Commands.SequenceEqual (commands)).Key; } /// Gets all bound to the set of commands specified by . /// The set of commands to search. diff --git a/Terminal.Gui/Input/Keyboard/KeyBinding.cs b/Terminal.Gui/Input/Keyboard/KeyBinding.cs index eb87b33813..59806ab169 100644 --- a/Terminal.Gui/Input/Keyboard/KeyBinding.cs +++ b/Terminal.Gui/Input/Keyboard/KeyBinding.cs @@ -36,6 +36,9 @@ public KeyBinding (Command [] commands, View? target, object? data = null) /// The commands this key binding will invoke. public Command [] Commands { get; set; } + /// + public object? Data { get; set; } + /// /// The Key that is bound to the . /// @@ -43,9 +46,4 @@ public KeyBinding (Command [] commands, View? target, object? data = null) /// The view the key binding is bound to. public View? Target { get; set; } - - /// - /// Arbitrary context that can be associated with this key binding. - /// - public object? Data { get; set; } } diff --git a/Terminal.Gui/Input/Mouse/MouseBinding.cs b/Terminal.Gui/Input/Mouse/MouseBinding.cs index 11689719a7..c4b7ad25d7 100644 --- a/Terminal.Gui/Input/Mouse/MouseBinding.cs +++ b/Terminal.Gui/Input/Mouse/MouseBinding.cs @@ -25,6 +25,9 @@ public MouseBinding (Command [] commands, MouseFlags mouseFlags) /// The commands this binding will invoke. public Command [] Commands { get; set; } + /// + public object? Data { get; set; } + /// /// The mouse event arguments. /// diff --git a/Terminal.Gui/Resources/GlobalResources.cs b/Terminal.Gui/Resources/GlobalResources.cs index b60836d9ac..625dd20c5d 100644 --- a/Terminal.Gui/Resources/GlobalResources.cs +++ b/Terminal.Gui/Resources/GlobalResources.cs @@ -66,5 +66,5 @@ static GlobalResources () /// /// /// Null if the resource was not found in the current culture or the invariant culture. - public static string GetString (string name, CultureInfo? culture = null!) { return _resourceManagerWrapper.GetString (name, culture); } + public static string? GetString (string name, CultureInfo? culture = null!) { return _resourceManagerWrapper.GetString (name, culture); } } diff --git a/Terminal.Gui/Resources/ResourceManagerWrapper.cs b/Terminal.Gui/Resources/ResourceManagerWrapper.cs index ff4eeeb35d..fcebe7b661 100644 --- a/Terminal.Gui/Resources/ResourceManagerWrapper.cs +++ b/Terminal.Gui/Resources/ResourceManagerWrapper.cs @@ -66,10 +66,10 @@ public object GetObject (string name, CultureInfo culture = null!) return filteredValue; } - public string GetString (string name, CultureInfo? culture = null!) + public string? GetString (string name, CultureInfo? culture = null!) { // Attempt to get the string for the specified culture - string value = _resourceManager.GetString (name, culture)!; + string? value = _resourceManager.GetString (name, culture)!; // If it's already using the invariant culture return if (Equals (culture, CultureInfo.InvariantCulture)) @@ -80,7 +80,7 @@ public string GetString (string name, CultureInfo? culture = null!) // If the string is empty or null, fall back to the invariant culture if (string.IsNullOrEmpty (value)) { - value = _resourceManager.GetString (name, CultureInfo.InvariantCulture)!; + value = _resourceManager.GetString (name, CultureInfo.InvariantCulture); } return value; diff --git a/Terminal.Gui/Resources/Strings.Designer.cs b/Terminal.Gui/Resources/Strings.Designer.cs index e88ae567cd..13fcfe02ca 100644 --- a/Terminal.Gui/Resources/Strings.Designer.cs +++ b/Terminal.Gui/Resources/Strings.Designer.cs @@ -159,6 +159,168 @@ internal static string charMapInfoDlgInfoLabel { } } + /// + /// Looks up a localized string similar to _Copy. + /// + internal static string cmd_Copy { + get { + return ResourceManager.GetString("cmd.Copy", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Copy to clipboard. + /// + internal static string cmd_Copy_Help { + get { + return ResourceManager.GetString("cmd.Copy.Help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cu_t. + /// + internal static string cmd_Cut { + get { + return ResourceManager.GetString("cmd.Cut", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cut to clipboard. + /// + internal static string cmd_Cut_Help { + get { + return ResourceManager.GetString("cmd.Cut.Help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to _New file. + /// + internal static string cmd_New { + get { + return ResourceManager.GetString("cmd.New", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to New file. + /// + internal static string cmd_New_Help { + get { + return ResourceManager.GetString("cmd.New.Help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to _Open.... + /// + internal static string cmd_Open { + get { + return ResourceManager.GetString("cmd.Open", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open a file. + /// + internal static string cmd_Open_Help { + get { + return ResourceManager.GetString("cmd.Open.Help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to _Paste. + /// + internal static string cmd_Paste { + get { + return ResourceManager.GetString("cmd.Paste", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Paste from clipboard. + /// + internal static string cmd_Paste_Help { + get { + return ResourceManager.GetString("cmd.Paste.Help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to E_xit. + /// + internal static string cmd_Quit { + get { + return ResourceManager.GetString("cmd.Quit", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to . + /// + internal static string cmd_Quit_Help { + get { + return ResourceManager.GetString("cmd.Quit.Help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to _Save. + /// + internal static string cmd_Save { + get { + return ResourceManager.GetString("cmd.Save", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Save. + /// + internal static string cmd_Save_Help { + get { + return ResourceManager.GetString("cmd.Save.Help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Save _As.... + /// + internal static string cmd_SaveAs { + get { + return ResourceManager.GetString("cmd.SaveAs", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Save file as. + /// + internal static string cmd_SaveAs_Help { + get { + return ResourceManager.GetString("cmd.SaveAs.Help", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to _Select all. + /// + internal static string cmd_SelectAll { + get { + return ResourceManager.GetString("cmd.SelectAll", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Select all. + /// + internal static string cmd_SelectAll_Help { + get { + return ResourceManager.GetString("cmd.SelectAll.Help", resourceCulture); + } + } + /// /// Looks up a localized string similar to Co_lors. /// diff --git a/Terminal.Gui/Resources/Strings.fr-FR.resx b/Terminal.Gui/Resources/Strings.fr-FR.resx index c20959da40..ef211929bd 100644 --- a/Terminal.Gui/Resources/Strings.fr-FR.resx +++ b/Terminal.Gui/Resources/Strings.fr-FR.resx @@ -183,4 +183,58 @@ Cou_leurs + + _Ouvrir + + + + + + _Enregistrer + + + E_nregistrer sous + + + Co_uper + + + _Copier + + + C_oller + + + Tout _sélectionner + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Terminal.Gui/Resources/Strings.ja-JP.resx b/Terminal.Gui/Resources/Strings.ja-JP.resx index fa4bda4210..2077179d48 100644 --- a/Terminal.Gui/Resources/Strings.ja-JP.resx +++ b/Terminal.Gui/Resources/Strings.ja-JP.resx @@ -279,4 +279,58 @@ 絵の具 (_L) + + 開く (_O) + + + + + + 保存 (_S) + + + 名前を付けて保存(_A) + + + 切り取り (_T) + + + コピー (_C) + + + + + + 全て選択 (_S) + + + 新規 (_N) + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Terminal.Gui/Resources/Strings.pt-PT.resx b/Terminal.Gui/Resources/Strings.pt-PT.resx index 28aabf522c..a10aef06bd 100644 --- a/Terminal.Gui/Resources/Strings.pt-PT.resx +++ b/Terminal.Gui/Resources/Strings.pt-PT.resx @@ -183,4 +183,58 @@ Co_res + + _Abrir + + + + + + _Guardar + + + Guardar _como + + + Cor_tar + + + _Copiar + + + Co_lar + + + _Selecionar Tudo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Terminal.Gui/Resources/Strings.resx b/Terminal.Gui/Resources/Strings.resx index e241b11f8e..bb0cd914ea 100644 --- a/Terminal.Gui/Resources/Strings.resx +++ b/Terminal.Gui/Resources/Strings.resx @@ -301,4 +301,58 @@ failed getting + + _Open... + + + E_xit + + + _Save + + + Save _As... + + + Cu_t + + + _Copy + + + _Paste + + + _Select all + + + _New file + + + Open a file + + + + + + Save + + + Save file as + + + Cut to clipboard + + + Copy to clipboard + + + Paste from clipboard + + + Select all + + + New file + \ No newline at end of file diff --git a/Terminal.Gui/Resources/Strings.zh-Hans.resx b/Terminal.Gui/Resources/Strings.zh-Hans.resx index 8ea63e91d6..7abb0c7bac 100644 --- a/Terminal.Gui/Resources/Strings.zh-Hans.resx +++ b/Terminal.Gui/Resources/Strings.zh-Hans.resx @@ -279,4 +279,58 @@ 旗帜 (_L) + + 打开 (_O) + + + + + + 保存 (_S) + + + 另存为 (_A) + + + 剪切 (_T) + + + 复制 (_C) + + + 粘贴 (_P) + + + 全选 (_S) + + + 新建 (_N) + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Terminal.Gui/View/SuperViewChangedEventArgs.cs b/Terminal.Gui/View/SuperViewChangedEventArgs.cs index 1090980794..c3406ff6a8 100644 --- a/Terminal.Gui/View/SuperViewChangedEventArgs.cs +++ b/Terminal.Gui/View/SuperViewChangedEventArgs.cs @@ -2,7 +2,7 @@ /// /// Args for events where the of a is changed (e.g. -/// / events). +/// ). /// public class SuperViewChangedEventArgs : EventArgs { @@ -19,8 +19,7 @@ public SuperViewChangedEventArgs (View superView, View subView) public View SubView { get; } /// - /// The parent. For this is the old parent (new parent now being null). For - /// it is the new parent to whom view now belongs. + /// The parent. For this is the old parent (new parent now being null). /// public View SuperView { get; } } diff --git a/Terminal.Gui/View/View.Adornments.cs b/Terminal.Gui/View/View.Adornments.cs index aa25bd24be..9e0549839d 100644 --- a/Terminal.Gui/View/View.Adornments.cs +++ b/Terminal.Gui/View/View.Adornments.cs @@ -203,10 +203,10 @@ public LineStyle BorderStyle /// /// For more advanced customization of the view's border, manipulate see directly. /// - /// - public virtual void SetBorderStyle (LineStyle value) + /// + public virtual void SetBorderStyle (LineStyle style) { - if (value != LineStyle.None) + if (style != LineStyle.None) { if (Border!.Thickness == Thickness.Empty) { @@ -218,7 +218,7 @@ public virtual void SetBorderStyle (LineStyle value) Border!.Thickness = new (0); } - Border.LineStyle = value; + Border.LineStyle = style; } /// diff --git a/Terminal.Gui/View/View.Command.cs b/Terminal.Gui/View/View.Command.cs index a273212fce..760315736a 100644 --- a/Terminal.Gui/View/View.Command.cs +++ b/Terminal.Gui/View/View.Command.cs @@ -14,6 +14,9 @@ public partial class View // Command APIs /// private void SetupCommands () { + // NotBound - Invoked if no handler is bound + AddCommand (Command.NotBound, RaiseCommandNotBound); + // Enter - Raise Accepted AddCommand (Command.Accept, RaiseAccepting); @@ -50,6 +53,45 @@ private void SetupCommands () }); } + /// + /// Called when a command that has not been bound is invoked. + /// + /// + /// if no event was raised; input processing should continue. + /// if the event was raised and was not handled (or cancelled); input processing should continue. + /// if the event was raised and handled (or cancelled); input processing should stop. + /// + protected bool? RaiseCommandNotBound (ICommandContext? ctx) + { + CommandEventArgs args = new () { Context = ctx }; + + // Best practice is to invoke the virtual method first. + // This allows derived classes to handle the event and potentially cancel it. + if (OnCommandNotBound (args) || args.Cancel) + { + return true; + } + + // If the event is not canceled by the virtual method, raise the event to notify any external subscribers. + CommandNotBound?.Invoke (this, args); + + return CommandNotBound is null ? null : args.Cancel; + } + + /// + /// Called when a command that has not been bound is invoked. + /// Set CommandEventArgs.Cancel to + /// and return to cancel the event. The default implementation does nothing. + /// + /// The event arguments. + /// to stop processing. + protected virtual bool OnCommandNotBound (CommandEventArgs args) { return false; } + + /// + /// Cancelable event raised when a command that has not been bound is invoked. + /// + public event EventHandler? CommandNotBound; + /// /// Called when the user is accepting the state of the View and the has been invoked. Calls which can be cancelled; if not cancelled raises . /// event. The default handler calls this method. @@ -95,7 +137,9 @@ private void SetupCommands () if (isDefaultView != this && isDefaultView is Button { IsDefault: true } button) { - bool? handled = isDefaultView.InvokeCommand (Command.Accept, new ([Command.Accept], null, this)); + // TODO: It's a bit of a hack that this uses KeyBinding. There should be an InvokeCommmand that + // TODO: is generic? + bool? handled = isDefaultView.InvokeCommand (Command.Accept, ctx); if (handled == true) { return true; @@ -104,7 +148,7 @@ private void SetupCommands () if (SuperView is { }) { - return SuperView?.InvokeCommand (Command.Accept, new ([Command.Accept], null, this)) is true; + return SuperView?.InvokeCommand (Command.Accept, ctx) is true; } } @@ -294,9 +338,7 @@ private void SetupCommands () { if (!_commandImplementations.ContainsKey (command)) { - throw new NotSupportedException ( - @$"A Binding was set up for the command {command} ({binding}) but that command is not supported by this View ({GetType ().Name})" - ); + Logging.Warning (@$"{command} is not supported by this View ({GetType ().Name}). Binding: {binding}."); } // each command has its own return value @@ -327,16 +369,36 @@ private void SetupCommands () /// public bool? InvokeCommand (Command command, TBindingType binding) { - if (_commandImplementations.TryGetValue (command, out CommandImplementation? implementation)) + if (!_commandImplementations.TryGetValue (command, out CommandImplementation? implementation)) { - return implementation (new CommandContext () - { - Command = command, - Binding = binding, - }); + _commandImplementations.TryGetValue (Command.NotBound, out implementation); } + return implementation! (new CommandContext () + { + Command = command, + Source = this, + Binding = binding, + }); + } - return null; + + /// + /// Invokes the specified command. + /// + /// The command to invoke. + /// The context to pass with the command. + /// + /// if no command was found; input processing should continue. + /// if the command was invoked and was not handled (or cancelled); input processing should continue. + /// if the command was invoked the command was handled (or cancelled); input processing should stop. + /// + public bool? InvokeCommand (Command command, ICommandContext? ctx) + { + if (!_commandImplementations.TryGetValue (command, out CommandImplementation? implementation)) + { + _commandImplementations.TryGetValue (Command.NotBound, out implementation); + } + return implementation! (ctx); } /// @@ -350,11 +412,12 @@ private void SetupCommands () /// public bool? InvokeCommand (Command command) { - if (_commandImplementations.TryGetValue (command, out CommandImplementation? implementation)) + if (!_commandImplementations.TryGetValue (command, out CommandImplementation? implementation)) { - return implementation (null); + _commandImplementations.TryGetValue (Command.NotBound, out implementation); } - return null; + return implementation! (null); + } } diff --git a/Terminal.Gui/View/View.Diagnostics.cs b/Terminal.Gui/View/View.Diagnostics.cs index 8b67e50a87..4057619d22 100644 --- a/Terminal.Gui/View/View.Diagnostics.cs +++ b/Terminal.Gui/View/View.Diagnostics.cs @@ -1,35 +1,6 @@ #nullable enable namespace Terminal.Gui; -/// Enables diagnostic functions for . -[Flags] -public enum ViewDiagnosticFlags : uint -{ - /// All diagnostics off - Off = 0b_0000_0000, - - /// - /// When enabled, will draw a ruler in the Thickness. See . - /// - Ruler = 0b_0000_0001, - - /// - /// When enabled, will draw the first letter of the Adornment name ('M', 'B', or 'P') - /// in the Thickness. See . - /// - Thickness = 0b_0000_0010, - - /// - /// When enabled the View's colors will be darker when the mouse is hovering over the View (See and . - /// - Hover = 0b_0000_00100, - - /// - /// When enabled a draw indicator will be shown; the indicator will change each time the View's Draw method is called with NeedsDraw set to true. - /// - DrawIndicator = 0b_0000_01000, -} - public partial class View { /// Gets or sets whether diagnostic information will be drawn. This is a bit-field of .e diagnostics. diff --git a/Terminal.Gui/View/View.Drawing.cs b/Terminal.Gui/View/View.Drawing.cs index 09954c701b..1a198a03d3 100644 --- a/Terminal.Gui/View/View.Drawing.cs +++ b/Terminal.Gui/View/View.Drawing.cs @@ -1,5 +1,6 @@ #nullable enable using System.ComponentModel; +using static Unix.Terminal.Curses; namespace Terminal.Gui; @@ -76,7 +77,7 @@ public void Draw (DrawContext? context = null) // TODO: Simplify/optimize SetAttribute system. DoSetAttribute (); - DoClearViewport (); + DoClearViewport (context); // ------------------------------------ // Draw the subviews first (order matters: SubViews, Text, Content) @@ -134,7 +135,6 @@ public void Draw (DrawContext? context = null) private void DoDrawAdornmentsSubViews () { - // NOTE: We do not support subviews of Margin? if (Border?.SubViews is { } && Border.Thickness != Thickness.Empty) @@ -188,8 +188,7 @@ private void DoDrawAdornments (Region? originalClip) if (Margin?.NeedsLayout == true) { Margin.NeedsLayout = false; - // BUGBUG: This should not use ClearFrame as that clears the insides too - Margin?.ClearFrame (); + Margin?.Thickness.Draw (FrameToScreen ()); Margin?.Parent?.SetSubViewNeedsDraw (); } @@ -316,31 +315,29 @@ public void SetNormalAttribute () #region ClearViewport - internal void DoClearViewport () + internal void DoClearViewport (DrawContext? context = null) { - if (ViewportSettings.HasFlag (ViewportSettings.Transparent)) - { - return; - } - - if (OnClearingViewport ()) + if (ViewportSettings.HasFlag (ViewportSettings.Transparent) || OnClearingViewport ()) { return; } - var dev = new DrawEventArgs (Viewport, Rectangle.Empty, null); + var dev = new DrawEventArgs (Viewport, Rectangle.Empty, context); ClearingViewport?.Invoke (this, dev); if (dev.Cancel) { + // BUGBUG: We should add the Viewport to context.DrawRegion here? SetNeedsDraw (); return; } - ClearViewport (); - - OnClearedViewport (); - ClearedViewport?.Invoke (this, new (Viewport, Viewport, null)); + if (!ViewportSettings.HasFlag (ViewportSettings.Transparent)) + { + ClearViewport (context); + OnClearedViewport (); + ClearedViewport?.Invoke (this, new (Viewport, Viewport, null)); + } } /// @@ -379,7 +376,7 @@ protected virtual void OnClearedViewport () { } /// the area outside the content to be visually distinct. /// /// - public void ClearViewport () + public void ClearViewport (DrawContext? context = null) { if (Driver is null) { @@ -397,6 +394,9 @@ public void ClearViewport () Attribute prev = SetAttribute (GetNormalColor ()); Driver.FillRect (toClear); + + // context.AddDrawnRectangle (toClear); + SetAttribute (prev); SetNeedsDraw (); } @@ -412,6 +412,7 @@ private void DoDrawText (DrawContext? context = null) return; } + // TODO: Get rid of this vf in lieu of the one above if (OnDrawingText ()) { return; @@ -544,6 +545,7 @@ private void DoDrawSubViews (DrawContext? context = null) return; } + // TODO: Get rid of this vf in lieu of the one above if (OnDrawingSubViews ()) { return; @@ -707,6 +709,9 @@ private void DoDrawComplete (DrawContext? context) // Exclude the Border and Padding from the clip ExcludeFromClip (Border?.Thickness.AsRegion (FrameToScreen ())); ExcludeFromClip (Padding?.Thickness.AsRegion (FrameToScreen ())); + + // QUESTION: This makes it so that no nesting of transparent views is possible, but is more correct? + //context = new DrawContext (); } else { @@ -721,6 +726,7 @@ private void DoDrawComplete (DrawContext? context) // In the non-transparent (typical case), we want to exclude the entire view area (borderFrame) from the clip ExcludeFromClip (borderFrame); + // BUGBUG: There looks like a bug in Region where this Union call is not adding the rectangle right // Update context.DrawnRegion to include the entire view (borderFrame), but clipped to our SuperView's viewport // This enables the SuperView to know what was drawn by this view. context?.AddDrawnRectangle (borderFrame); diff --git a/Terminal.Gui/View/View.Keyboard.cs b/Terminal.Gui/View/View.Keyboard.cs index a47b333a1f..095ff6946a 100644 --- a/Terminal.Gui/View/View.Keyboard.cs +++ b/Terminal.Gui/View/View.Keyboard.cs @@ -302,9 +302,9 @@ public bool NewKeyDownEvent (Key key) return true; } - bool? handled = false; + bool? handled = InvokeCommandsBoundToHotKey (key); - if (InvokeCommandsBoundToHotKey (key, ref handled)) + if (handled is true) { return true; } @@ -590,10 +590,16 @@ private static bool InvokeCommandsBoundToKeyOnAdornment (Adornment adornment, Ke /// Invokes any commands bound to on this view and subviews. /// /// - /// - /// - internal bool InvokeCommandsBoundToHotKey (Key hotKey, ref bool? handled) + /// + /// if no command was invoked; input processing should continue. + /// if at least one command was invoked and was not handled (or cancelled); input processing + /// should continue. + /// if at least one command was invoked and handled (or cancelled); input processing should + /// stop. + /// + internal bool? InvokeCommandsBoundToHotKey (Key hotKey) { + bool? handled = null; // Process this View if (HotKeyBindings.TryGet (hotKey, out KeyBinding binding)) { @@ -604,16 +610,16 @@ internal bool InvokeCommandsBoundToHotKey (Key hotKey, ref bool? handled) } // Now, process any HotKey bindings in the subviews - foreach (View subview in InternalSubViews) + foreach (View subview in InternalSubViews.ToList()) { if (subview == Focused) { continue; } - bool recurse = subview.InvokeCommandsBoundToHotKey (hotKey, ref handled); + bool? recurse = subview.InvokeCommandsBoundToHotKey (hotKey); - if (recurse || (handled is { } && (bool)handled)) + if (recurse is true || (handled is { } && (bool)handled)) { return true; } @@ -644,27 +650,5 @@ internal bool InvokeCommandsBoundToHotKey (Key hotKey, ref bool? handled) return InvokeCommands (binding.Commands, binding); } - /// - /// Invokes the Commands bound to . - /// See for an overview of Terminal.Gui keyboard APIs. - /// - /// The hot key event passed. - /// - /// if no command was invoked; input processing should continue. - /// if at least one command was invoked and was not handled (or cancelled); input processing - /// should continue. - /// if at least one command was invoked and handled (or cancelled); input processing should - /// stop. - /// - protected bool? InvokeCommandsBoundToHotKey (Key hotKey) - { - if (!HotKeyBindings.TryGet (hotKey, out KeyBinding binding)) - { - return null; - } - - return InvokeCommands (binding.Commands, binding); - } - #endregion Key Bindings } diff --git a/Terminal.Gui/View/View.Layout.cs b/Terminal.Gui/View/View.Layout.cs index 5ae60b8ed6..988ddeb52f 100644 --- a/Terminal.Gui/View/View.Layout.cs +++ b/Terminal.Gui/View/View.Layout.cs @@ -1020,6 +1020,7 @@ private Size GetContainerSize () // BUGBUG: This method interferes with Dialog/MessageBox default min/max size. // TODO: Get rid of MenuBar coupling as part of https://github.com/gui-cs/Terminal.Gui/issues/2975 + // TODO: Refactor / rewrite this - It's a mess /// /// Gets a new location of the that is within the Viewport of the 's /// (e.g. for dragging a Window). The `out` parameters are the new X and Y coordinates. @@ -1048,7 +1049,7 @@ out int ny int maxDimension; View? superView; - if (viewToMove is not Toplevel || viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top) + if (viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top) { maxDimension = Application.Screen.Width; superView = Application.Top; @@ -1070,14 +1071,14 @@ out int ny nx = Math.Max (targetX, 0); nx = nx + viewToMove.Frame.Width > maxDimension ? Math.Max (maxDimension - viewToMove.Frame.Width, 0) : nx; - if (nx > viewToMove.Frame.X + viewToMove.Frame.Width) - { - nx = Math.Max (viewToMove.Frame.Right, 0); - } + //if (nx > viewToMove.Frame.X + viewToMove.Frame.Width) + //{ + // nx = Math.Max (viewToMove.Frame.Right, 0); + //} } else { - nx = targetX; + nx = 0;//targetX; } //System.Diagnostics.Debug.WriteLine ($"nx:{nx}, rWidth:{rWidth}"); @@ -1136,15 +1137,19 @@ out int ny ? Math.Max (maxDimension - viewToMove.Frame.Height, menuVisible ? 1 : 0) : ny; - if (ny > viewToMove.Frame.Y + viewToMove.Frame.Height) - { - ny = Math.Max (viewToMove.Frame.Bottom, 0); - } + //if (ny > viewToMove.Frame.Y + viewToMove.Frame.Height) + //{ + // ny = Math.Max (viewToMove.Frame.Bottom, 0); + //} + } + else + { + ny = 0; } - //System.Diagnostics.Debug.WriteLine ($"ny:{ny}, rHeight:{rHeight}"); + //System.Diagnostics.Debug.WriteLine ($"ny:{ny}, rHeight:{rHeight}"); - return superView!; + return superView!; } #endregion Utilities diff --git a/Terminal.Gui/View/View.Mouse.cs b/Terminal.Gui/View/View.Mouse.cs index b0f802af8e..7cb77f65b5 100644 --- a/Terminal.Gui/View/View.Mouse.cs +++ b/Terminal.Gui/View/View.Mouse.cs @@ -560,7 +560,7 @@ internal bool WhenGrabbedHandleClicked (MouseEventArgs mouseEvent) if (!WantMousePositionReports && Viewport.Contains (mouseEvent.Position)) { - return RaiseMouseClickEvent (mouseEvent); + return RaiseMouseClickEvent (mouseEvent); } return mouseEvent.Handled = true; @@ -770,11 +770,23 @@ internal bool SetPressedHighlight (HighlightStyle newHighlightStyle) View? start = Application.Top; + // PopoverHost - If visible, start with it instead of Top + if (Application.Popover?.GetActivePopover () is View {Visible: true } visiblePopover && !ignoreTransparent) + { + start = visiblePopover; + + // Put Top on stack next + viewsUnderMouse.Add (Application.Top); + } + Point currentLocation = location; while (start is { Visible: true } && start.Contains (currentLocation)) { - viewsUnderMouse.Add (start); + if (!start.ViewportSettings.HasFlag(ViewportSettings.TransparentMouse)) + { + viewsUnderMouse.Add (start); + } Adornment? found = null; @@ -825,13 +837,14 @@ internal bool SetPressedHighlight (HighlightStyle newHighlightStyle) if (subview is null) { + // In the case start is transparent, recursively add all it's subviews etc... if (start.ViewportSettings.HasFlag (ViewportSettings.TransparentMouse)) { viewsUnderMouse.AddRange (View.GetViewsUnderMouse (location, true)); // De-dupe viewsUnderMouse - HashSet dedupe = [..viewsUnderMouse]; - viewsUnderMouse = [..dedupe]; + HashSet hashSet = [.. viewsUnderMouse]; + viewsUnderMouse = [.. hashSet]; } // No subview was found that's under the mouse, so we're done diff --git a/Terminal.Gui/View/View.Navigation.cs b/Terminal.Gui/View/View.Navigation.cs index 7cce949d49..3ef975f6f2 100644 --- a/Terminal.Gui/View/View.Navigation.cs +++ b/Terminal.Gui/View/View.Navigation.cs @@ -315,6 +315,25 @@ public View? Focused } } + internal void RaiseFocusedChanged (View? previousFocused, View? focused) + { + //Logging.Trace($"RaiseFocusedChanged: {focused.Title}"); + OnFocusedChanged (previousFocused, focused); + FocusedChanged?.Invoke (this, new HasFocusEventArgs (true, true, previousFocused, focused)); + } + + /// + /// Called when the focused view has changed. + /// + /// + /// + protected virtual void OnFocusedChanged (View? previousFocused, View? focused) { } + + /// + /// Raised when the focused view has changed. + /// + public event EventHandler? FocusedChanged; + /// Returns a value indicating if this View is currently on Top (Active) public bool IsCurrentTop => Application.Top == this; @@ -373,6 +392,14 @@ internal bool RestoreFocus () return false; } + /// + /// Clears any focus state (e.g. the previously focused subview) from this view. + /// + public void ClearFocus () + { + _previouslyFocused = null; + } + private View? FindDeepestFocusableView (NavigationDirection direction, TabBehavior? behavior) { View [] indicies = GetFocusChain (direction, behavior); @@ -853,6 +880,7 @@ private void SetHasFocusFalse (View? newFocusedView, bool traversingDown = false private void RaiseFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView) { + // If we are the most focused view, we need to set the focused view in Application.Navigation if (newHasFocus && focusedView?.Focused is null) { Application.Navigation?.SetFocused (focusedView); @@ -864,6 +892,11 @@ private void RaiseFocusChanged (bool newHasFocus, View? previousFocusedView, Vie // Raise the event var args = new HasFocusEventArgs (newHasFocus, newHasFocus, previousFocusedView, focusedView); HasFocusChanged?.Invoke (this, args); + + if (newHasFocus || focusedView is null) + { + SuperView?.RaiseFocusedChanged (previousFocusedView, focusedView); + } } /// diff --git a/Terminal.Gui/View/View.cs b/Terminal.Gui/View/View.cs index 45f227cf7d..21885b6fd3 100644 --- a/Terminal.Gui/View/View.cs +++ b/Terminal.Gui/View/View.cs @@ -337,6 +337,8 @@ public virtual bool Visible if (!_visible) { + // BUGBUG: Ideally we'd reset _previouslyFocused to the first focusable subview + _previouslyFocused = SubViews.FirstOrDefault(v => v.CanFocus); if (HasFocus) { HasFocus = false; diff --git a/Terminal.Gui/View/ViewArrangement.cs b/Terminal.Gui/View/ViewArrangement.cs index 921fe1af9c..b43703ec2b 100644 --- a/Terminal.Gui/View/ViewArrangement.cs +++ b/Terminal.Gui/View/ViewArrangement.cs @@ -70,5 +70,5 @@ public enum ViewArrangement /// Use Ctrl-Tab (Ctrl-PageDown) / Ctrl-Shift-Tab (Ctrl-PageUp) to move between overlapped views. /// /// - Overlapped = 32 + Overlapped = 32, } diff --git a/Terminal.Gui/View/ViewDiagnosticFlags.cs b/Terminal.Gui/View/ViewDiagnosticFlags.cs new file mode 100644 index 0000000000..3c08030a68 --- /dev/null +++ b/Terminal.Gui/View/ViewDiagnosticFlags.cs @@ -0,0 +1,31 @@ +#nullable enable +namespace Terminal.Gui; + +/// Enables diagnostic functions for . +[Flags] +public enum ViewDiagnosticFlags : uint +{ + /// All diagnostics off + Off = 0b_0000_0000, + + /// + /// When enabled, will draw a ruler in the Thickness. See . + /// + Ruler = 0b_0000_0001, + + /// + /// When enabled, will draw the first letter of the Adornment name ('M', 'B', or 'P') + /// in the Thickness. See . + /// + Thickness = 0b_0000_0010, + + /// + /// When enabled the View's colors will be darker when the mouse is hovering over the View (See and . + /// + Hover = 0b_0000_00100, + + /// + /// When enabled a draw indicator will be shown; the indicator will change each time the View's Draw method is called with NeedsDraw set to true. + /// + DrawIndicator = 0b_0000_01000, +} diff --git a/Terminal.Gui/Views/Bar.cs b/Terminal.Gui/Views/Bar.cs index 0fe01a5245..00cdaf5751 100644 --- a/Terminal.Gui/Views/Bar.cs +++ b/Terminal.Gui/Views/Bar.cs @@ -20,7 +20,7 @@ public class Bar : View, IOrientation, IDesignable public Bar () : this ([]) { } /// - public Bar (IEnumerable? shortcuts) + public Bar (IEnumerable? shortcuts) { CanFocus = true; @@ -32,9 +32,10 @@ public Bar (IEnumerable? shortcuts) // Initialized += Bar_Initialized; MouseEvent += OnMouseEvent; + if (shortcuts is { }) { - foreach (Shortcut shortcut in shortcuts) + foreach (View shortcut in shortcuts) { Add (shortcut); } @@ -81,13 +82,14 @@ public override void EndInit () } /// - public override void SetBorderStyle (LineStyle value) + public override void SetBorderStyle (LineStyle lineStyle) { if (Border is { }) { // The default changes the thickness. We don't want that. We just set the style. - Border.LineStyle = value; + Border.LineStyle = lineStyle; } + //base.SetBorderStyle(lineStyle); } #region IOrientation members @@ -217,7 +219,13 @@ private void LayoutBarItems (Size contentSize) barItem.ColorScheme = ColorScheme; barItem.X = Pos.Align (Alignment.Start, AlignmentModes); barItem.Y = 0; //Pos.Center (); + + if (barItem is Shortcut sc) + { + sc.Width = sc.GetWidthDimAuto (); + } } + break; case Orientation.Vertical: @@ -278,7 +286,7 @@ private void LayoutBarItems (Size contentSize) { if (subView is not Line) { - subView.Width = Dim.Auto (DimAutoStyle.Auto, minimumContentDim: maxBarItemWidth); + subView.Width = Dim.Auto (DimAutoStyle.Auto, minimumContentDim: maxBarItemWidth, maximumContentDim: maxBarItemWidth); } } } @@ -298,7 +306,7 @@ private void LayoutBarItems (Size contentSize) } /// - public bool EnableForDesign () + public virtual bool EnableForDesign () { var shortcut = new Shortcut { diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs index 7738d21362..d4f1a186bb 100644 --- a/Terminal.Gui/Views/Button.cs +++ b/Terminal.Gui/Views/Button.cs @@ -1,7 +1,7 @@ namespace Terminal.Gui; /// -/// A button View that can be pressed with the mouse or keybaord. +/// A button View that can be pressed with the mouse or keyboard. /// /// /// diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs index 6def5670da..2cfe37103e 100644 --- a/Terminal.Gui/Views/ComboBox.cs +++ b/Terminal.Gui/Views/ComboBox.cs @@ -80,11 +80,7 @@ public ComboBox () // Things this view knows how to do AddCommand (Command.Accept, (ctx) => { - if (ctx is not CommandContext keyCommandContext) - { - return false; - } - if (keyCommandContext.Binding.Data == _search) + if (ctx?.Source == _search) { return null; } @@ -93,8 +89,8 @@ public ComboBox () AddCommand (Command.Toggle, () => ExpandCollapse ()); AddCommand (Command.Expand, () => Expand ()); AddCommand (Command.Collapse, () => Collapse ()); - AddCommand (Command.Down, () => MoveDown ()); - AddCommand (Command.Up, () => MoveUp ()); + AddCommand (Command.Down, MoveDown); + AddCommand (Command.Up, MoveUp); AddCommand (Command.PageDown, () => PageDown ()); AddCommand (Command.PageUp, () => PageUp ()); AddCommand (Command.Start, () => MoveHome ()); @@ -511,7 +507,7 @@ private void HideList () } Reset (true); - _listview.ClearViewport (); + _listview.ClearViewport (null); _listview.TabStop = TabBehavior.NoStop; SuperView?.MoveSubViewToStart (this); @@ -812,7 +808,7 @@ private void ShowList () _listview.SetSource (_searchSet); _listview.ResumeSuspendCollectionChangedEvent (); - _listview.ClearViewport (); + _listview.ClearViewport (null); _listview.Height = CalculateHeight (); SuperView?.MoveSubViewToStart (this); } diff --git a/Terminal.Gui/Views/Menu/ContextMenuv2.cs b/Terminal.Gui/Views/Menu/ContextMenuv2.cs new file mode 100644 index 0000000000..994aec4a01 --- /dev/null +++ b/Terminal.Gui/Views/Menu/ContextMenuv2.cs @@ -0,0 +1,104 @@ +#nullable enable + +using System.Diagnostics; + +namespace Terminal.Gui; + +/// +/// ContextMenuv2 provides a Popover menu that can be positioned anywhere within a . +/// +/// To show the ContextMenu, set to the ContextMenu object and set +/// property to . +/// +/// +/// The menu will be hidden when the user clicks outside the menu or when the user presses . +/// +/// +/// To explicitly hide the menu, set property to . +/// +/// +/// is the key used to activate the ContextMenus (Shift+F10 by default). Callers can use this in +/// their keyboard handling code. +/// +/// The menu will be displayed at the current mouse coordinates. +/// +public class ContextMenuv2 : PopoverMenu, IDesignable +{ + + /// + /// The mouse flags that will trigger the context menu. The default is which is typically the right mouse button. + /// + public MouseFlags MouseFlags { get; set; } = MouseFlags.Button3Clicked; + + /// Initializes a context menu with no menu items. + public ContextMenuv2 () : this ([]) { } + + /// + public ContextMenuv2 (Menuv2? menu) : base (menu) + { + Key = DefaultKey; + } + + /// + public ContextMenuv2 (IEnumerable? menuItems) : this (new Menuv2 (menuItems)) + { + } + + private Key _key = DefaultKey; + + /// Specifies the key that will activate the context menu. + public Key Key + { + get => _key; + set + { + Key oldKey = _key; + _key = value; + KeyChanged?.Invoke (this, new KeyChangedEventArgs (oldKey, _key)); + } + } + + /// Event raised when the is changed. + public event EventHandler? KeyChanged; + + /// + public bool EnableForDesign () + { + var shortcut = new Shortcut + { + Text = "Quit", + Title = "Q_uit", + Key = Key.Z.WithCtrl, + }; + + Add (shortcut); + + shortcut = new Shortcut + { + Text = "Help Text", + Title = "Help", + Key = Key.F1, + }; + + Add (shortcut); + + shortcut = new Shortcut + { + Text = "Czech", + CommandView = new CheckBox () + { + Title = "_Check" + }, + Key = Key.F9, + CanFocus = false + }; + + Add (shortcut); + + // HACK: This enables All Views Tester to show the CM if DefaultKey is pressed + AddCommand (Command.Context, () => Visible = true); + HotKeyBindings.Add (DefaultKey, Command.Context); + + return true; + } +} diff --git a/Terminal.Gui/Views/Menu/Menu.cs b/Terminal.Gui/Views/Menu/Menu.cs index 3f1e406fb5..b7d14d3f0b 100644 --- a/Terminal.Gui/Views/Menu/Menu.cs +++ b/Terminal.Gui/Views/Menu/Menu.cs @@ -252,8 +252,7 @@ protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocus protected override bool OnKeyDownNotHandled (Key keyEvent) { // We didn't handle the key, pass it on to host - bool? handled = null; - return _host.InvokeCommandsBoundToHotKey (keyEvent, ref handled) == true; + return _host.InvokeCommandsBoundToHotKey (keyEvent) is true; } protected override bool OnMouseEvent (MouseEventArgs me) diff --git a/Terminal.Gui/Views/Menu/MenuBarItemv2.cs b/Terminal.Gui/Views/Menu/MenuBarItemv2.cs new file mode 100644 index 0000000000..6fe9f121eb --- /dev/null +++ b/Terminal.Gui/Views/Menu/MenuBarItemv2.cs @@ -0,0 +1,98 @@ +#nullable enable + +namespace Terminal.Gui; + +/// +/// A -derived object to be used as items in a . +/// MenuBarItems have a title, a hotkey, and an action to execute on activation. +/// +public class MenuBarItemv2 : MenuItemv2 +{ + /// + /// Creates a new instance of . + /// + public MenuBarItemv2 () : base (null, Command.NotBound) { } + + /// + /// Creates a new instance of . Each MenuBarItem typically has a + /// that is + /// shown when the item is selected. + /// + /// + /// + /// + /// The View that will be invoked on when user does something that causes the MenuBarItems's + /// Accept event to be raised. + /// + /// + /// The Command to invoke on . The Key + /// has bound to will be used as + /// + /// The text to display for the command. + /// The Popover Menu that will be displayed when this item is selected. + public MenuBarItemv2 (View? targetView, Command command, string? commandText, PopoverMenu? popoverMenu = null) + : base ( + targetView, + command, + commandText) + { + TargetView = targetView; + Command = command; + PopoverMenu = popoverMenu; + } + + /// + /// Creates a new instance of with the specified . This is a + /// helper for the most common MenuBar use-cases. + /// + /// + /// + /// The text to display for the command. + /// The Popover Menu that will be displayed when this item is selected. + public MenuBarItemv2 (string commandText, PopoverMenu? popoverMenu = null) + : this ( + null, + Command.NotBound, + commandText, + popoverMenu) + { } + + /// + /// Creates a new instance of with the automatcialy added to a + /// . + /// This is a helper for the most common MenuBar use-cases. + /// + /// + /// + /// The text to display for the command. + /// + /// The menu items that will be added to the Popover Menu that will be displayed when this item is + /// selected. + /// + public MenuBarItemv2 (string commandText, IEnumerable menuItems) + : this ( + null, + Command.NotBound, + commandText, + new (new (menuItems))) + { } + + // TODO: Hide base.SubMenu? + + /// + /// The Popover Menu that will be displayed when this item is selected. + /// + public PopoverMenu? PopoverMenu { get; set; } + + /// + protected override void Dispose (bool disposing) + { + if (disposing) + { + PopoverMenu?.Dispose (); + PopoverMenu = null; + } + + base.Dispose (disposing); + } +} diff --git a/Terminal.Gui/Views/Menu/MenuBarv2.cs b/Terminal.Gui/Views/Menu/MenuBarv2.cs new file mode 100644 index 0000000000..a129321d5d --- /dev/null +++ b/Terminal.Gui/Views/Menu/MenuBarv2.cs @@ -0,0 +1,342 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// A horizontal list of s. Each can have a +/// that is shown when the is selected. +/// +/// +/// MenuBars may be hosted by any View and will, by default, be positioned the full width across the top of the View's +/// Viewport. +/// +public class MenuBarv2 : Menuv2, IDesignable +{ + /// + public MenuBarv2 () : this ([]) { } + + /// + public MenuBarv2 (IEnumerable menuBarItems) : base (menuBarItems) + { + TabStop = TabBehavior.TabGroup; + Y = 0; + Width = Dim.Fill (); + Orientation = Orientation.Horizontal; + + AddCommand (Command.Right, MoveRight); + KeyBindings.Add (Key.CursorRight, Command.Right); + + AddCommand (Command.Left, MoveLeft); + KeyBindings.Add (Key.CursorLeft, Command.Left); + + return; + + bool? MoveLeft (ICommandContext? ctx) { return AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop); } + + bool? MoveRight (ICommandContext? ctx) { return AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); } + } + + /// + protected override void OnSelectedMenuItemChanged (MenuItemv2? selected) + { + if (selected is MenuBarItemv2 { } selectedMenuBarItem) + { + ShowPopover (selectedMenuBarItem); + } + } + + /// + public override void EndInit () + { + base.EndInit (); + + if (Border is { }) + { + Border.Thickness = new (0); + Border.LineStyle = LineStyle.None; + } + + // TODO: This needs to be done whenever a menuitem in any memubaritem changes + foreach (MenuBarItemv2? mbi in SubViews.Select(s => s as MenuBarItemv2)) + { + Application.Popover?.Register (mbi?.PopoverMenu); + } + } + + /// + protected override bool OnAccepting (CommandEventArgs args) + { + if (args.Context?.Source is MenuBarItemv2 { PopoverMenu: { } } menuBarItem) + { + ShowPopover (menuBarItem); + } + + return base.OnAccepting (args); + } + + private void ShowPopover (MenuBarItemv2? menuBarItem) + { + if (menuBarItem?.PopoverMenu is { IsInitialized: false }) + { + menuBarItem.PopoverMenu.BeginInit (); + menuBarItem.PopoverMenu.EndInit (); + } + + // If the active popover is a PopoverMenu and part of this MenuBar... + if (menuBarItem?.PopoverMenu is null + && Application.Popover?.GetActivePopover () is PopoverMenu popoverMenu + && popoverMenu?.Root?.SuperMenuItem?.SuperView == this) + { + Application.Popover?.HidePopover (popoverMenu); + } + + menuBarItem?.PopoverMenu?.MakeVisible (new Point (menuBarItem.FrameToScreen ().X, menuBarItem.FrameToScreen ().Bottom)); + + if (menuBarItem?.PopoverMenu?.Root is { }) + { + menuBarItem.PopoverMenu.Root.SuperMenuItem = menuBarItem; + } + } + + /// + public bool EnableForDesign (ref readonly TContext context) where TContext : notnull + { + Add ( + new MenuBarItemv2 ( + "_File", + [ + new MenuItemv2 (this, Command.New), + new MenuItemv2 (this, Command.Open), + new MenuItemv2 (this, Command.Save), + new MenuItemv2 (this, Command.SaveAs), + new Line (), + new MenuItemv2 + { + Title = "_Preferences", + SubMenu = new ( + [ + new MenuItemv2 + { + CommandView = new CheckBox () + { + Title = "O_ption", + }, + HelpText = "Toggle option" + }, + new MenuItemv2 + { + Title = "_Settings...", + HelpText = "More settings", + Action = () => MessageBox.Query ("Settings", "This is the Settings Dialog\n", ["_Ok", "_Cancel"]) + } + ] + ) + }, + new Line (), + new MenuItemv2 (this, Command.Quit) + ] + ) + ); + + Add ( + new MenuBarItemv2 ( + "_Edit", + [ + new MenuItemv2 (this, Command.Cut), + new MenuItemv2 (this, Command.Copy), + new MenuItemv2 (this, Command.Paste), + new Line (), + new MenuItemv2 (this, Command.SelectAll) + ] + ) + ); + + Add ( + new MenuBarItemv2 ( + "_Help", + [ + new MenuItemv2 + { + Title = "_Online Help...", + Action = () => MessageBox.Query ("Online Help", "https://gui-cs.github.io/Terminal.GuiV2Docs", "Ok") + }, + new MenuItemv2 + { + Title = "About...", + Action = () => MessageBox.Query ("About", "Something About Mary.", "Ok") + } + ] + ) + ); + + // if (context is not Func actionFn) + // { + // actionFn = (_) => true; + // } + + // View? targetView = context as View; + + // Add (new MenuItemv2 (targetView, + // Command.NotBound, + // "_File", + // new MenuItem [] + // { + // new ( + // "_New", + // "", + // () => actionFn ("New"), + // null, + // null, + // KeyCode.CtrlMask | KeyCode.N + // ), + // new ( + // "_Open", + // "", + // () => actionFn ("Open"), + // null, + // null, + // KeyCode.CtrlMask | KeyCode.O + // ), + // new ( + // "_Save", + // "", + // () => actionFn ("Save"), + // null, + // null, + // KeyCode.CtrlMask | KeyCode.S + // ), + //#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. + // null, + //#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. + + // // Don't use Application.Quit so we can disambiguate between quitting and closing the toplevel + // new ( + // "_Quit", + // "", + // () => actionFn ("Quit"), + // null, + // null, + // KeyCode.CtrlMask | KeyCode.Q + // ) + // } + // ), + // new MenuBarItem ( + // "_Edit", + // new MenuItem [] + // { + // new ( + // "_Copy", + // "", + // () => actionFn ("Copy"), + // null, + // null, + // KeyCode.CtrlMask | KeyCode.C + // ), + // new ( + // "C_ut", + // "", + // () => actionFn ("Cut"), + // null, + // null, + // KeyCode.CtrlMask | KeyCode.X + // ), + // new ( + // "_Paste", + // "", + // () => actionFn ("Paste"), + // null, + // null, + // KeyCode.CtrlMask | KeyCode.V + // ), + // new MenuBarItem ( + // "_Find and Replace", + // new MenuItem [] + // { + // new ( + // "F_ind", + // "", + // () => actionFn ("Find"), + // null, + // null, + // KeyCode.CtrlMask | KeyCode.F + // ), + // new ( + // "_Replace", + // "", + // () => actionFn ("Replace"), + // null, + // null, + // KeyCode.CtrlMask | KeyCode.H + // ), + // new MenuBarItem ( + // "_3rd Level", + // new MenuItem [] + // { + // new ( + // "_1st", + // "", + // () => actionFn ( + // "1" + // ), + // null, + // null, + // KeyCode.F1 + // ), + // new ( + // "_2nd", + // "", + // () => actionFn ( + // "2" + // ), + // null, + // null, + // KeyCode.F2 + // ) + // } + // ), + // new MenuBarItem ( + // "_4th Level", + // new MenuItem [] + // { + // new ( + // "_5th", + // "", + // () => actionFn ( + // "5" + // ), + // null, + // null, + // KeyCode.CtrlMask + // | KeyCode.D5 + // ), + // new ( + // "_6th", + // "", + // () => actionFn ( + // "6" + // ), + // null, + // null, + // KeyCode.CtrlMask + // | KeyCode.D6 + // ) + // } + // ) + // } + // ), + // new ( + // "_Select All", + // "", + // () => actionFn ("Select All"), + // null, + // null, + // KeyCode.CtrlMask + // | KeyCode.ShiftMask + // | KeyCode.S + // ) + // } + // ), + // new MenuBarItem ("_About", "Top-Level", () => actionFn ("About")) + // ]; + return true; + } +} diff --git a/Terminal.Gui/Views/Menu/MenuItemv2.cs b/Terminal.Gui/Views/Menu/MenuItemv2.cs new file mode 100644 index 0000000000..acf0311272 --- /dev/null +++ b/Terminal.Gui/Views/Menu/MenuItemv2.cs @@ -0,0 +1,200 @@ +#nullable enable + +using System.ComponentModel; +using Terminal.Gui.Resources; + +namespace Terminal.Gui; + +/// +/// A -dervied object to be used as a menu item in a . Has title, an +/// associated help text, and an action to execute on activation. +/// +public class MenuItemv2 : Shortcut +{ + /// + /// Creates a new instance of . + /// + public MenuItemv2 () : base (Key.Empty, null, null) { } + + /// + /// Creates a new instance of , binding it to and + /// . The Key + /// has bound to will be used as . + /// + /// + /// + /// + /// The View that will be invoked on when user does something that causes the Shortcut's + /// Accept + /// event to be raised. + /// + /// + /// The Command to invoke on . The Key + /// has bound to will be used as + /// + /// The text to display for the command. + /// The help text to display. + /// The submenu to display when the user selects this menu item. + public MenuItemv2 (View? targetView, Command command, string? commandText = null, string? helpText = null, Menuv2? subMenu = null) + : base ( + targetView?.HotKeyBindings.GetFirstFromCommands (command)!, + string.IsNullOrEmpty (commandText) ? GlobalResources.GetString ($"cmd.{command}") : commandText, + null, + string.IsNullOrEmpty (helpText) ? GlobalResources.GetString ($"cmd.{command}.Help") : helpText + ) + { + TargetView = targetView; + Command = command; + + SubMenu = subMenu; + } + + // TODO: Consider moving TargetView and Command to Shortcut? + + /// + /// Gets the target that the will be invoked on. + /// + public View? TargetView { get; set; } + + private Command _command; + + /// + /// Gets the that will be invoked on when the MenuItem is selected. + /// + public Command Command + { + get => _command; + set + { + if (_command == value) + { + return; + } + + _command = value; + + if (string.IsNullOrEmpty (Title)) + { + Title = GlobalResources.GetString ($"cmd.{_command}") ?? string.Empty; + } + + if (string.IsNullOrEmpty (HelpText)) + { + HelpText = GlobalResources.GetString ($"cmd.{_command}.Help") ?? string.Empty; + } + } + } + + internal override bool? DispatchCommand (ICommandContext? commandContext) + { + bool? ret = null; + + if (commandContext is { Command: not Command.HotKey }) + { + if (TargetView is { }) + { + commandContext.Command = Command; + ret = TargetView.InvokeCommand (Command, commandContext); + } + else + { + // Is this an Application-bound command? + ret = Application.InvokeCommandsBoundToKey (Key); + } + } + + if (ret is not true) + { + ret = base.DispatchCommand (commandContext); + } + + Logging.Trace ($"{commandContext?.Source?.Title}"); + + RaiseAccepted (commandContext); + + return ret; + } + + private Menuv2? _subMenu; + + /// + /// The submenu to display when the user selects this menu item. + /// + public Menuv2? SubMenu + { + get => _subMenu; + set + { + _subMenu = value; + + if (_subMenu is { }) + { + // TODO: This is a temporary hack - add a flag or something instead + KeyView.Text = $"{Glyphs.RightArrow}"; + _subMenu.SuperMenuItem = this; + } + } + } + + /// + protected override bool OnMouseEnter (CancelEventArgs eventArgs) + { + // When the mouse enters a menuitem, we set focus to it automatically. + + // Logging.Trace($"OnEnter {Title}"); + SetFocus (); + + return base.OnMouseEnter (eventArgs); + } + + // TODO: Consider moving Accepted to Shortcut? + + /// + /// Riases the / event indicating this item (or submenu) + /// was accepted. This is used to determine when to hide the menu. + /// + /// + /// + protected bool? RaiseAccepted (ICommandContext? ctx) + { + Logging.Trace ($"RaiseAccepted: {ctx}"); + CommandEventArgs args = new () { Context = ctx }; + + OnAccepted (args); + Accepted?.Invoke (this, args); + + return true; + } + + /// + /// Called when the user has accepted an item in this menu (or submenu. This is used to determine when to hide the + /// menu. + /// + /// + /// + /// + protected virtual void OnAccepted (CommandEventArgs args) { } + + /// + /// Raised when the user has accepted an item in this menu (or submenu. This is used to determine when to hide the + /// menu. + /// + /// + /// + /// See for more information. + /// + /// + public event EventHandler? Accepted; + + /// + protected override void Dispose (bool disposing) + { + if (disposing) + { + SubMenu?.Dispose (); + SubMenu = null; + } + + base.Dispose (disposing); + } +} diff --git a/Terminal.Gui/Views/Menu/Menuv2.cs b/Terminal.Gui/Views/Menu/Menuv2.cs new file mode 100644 index 0000000000..88df61b673 --- /dev/null +++ b/Terminal.Gui/Views/Menu/Menuv2.cs @@ -0,0 +1,172 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// A -derived object to be used as a verticaly-oriented menu. Each subview is a . +/// +public class Menuv2 : Bar +{ + /// + public Menuv2 () : this ([]) { } + + /// + public Menuv2 (IEnumerable? shortcuts) : base (shortcuts) + { + Orientation = Orientation.Vertical; + Width = Dim.Auto (); + Height = Dim.Auto (DimAutoStyle.Content, 1); + + Border!.Thickness = new Thickness (1, 1, 1, 1); + Border.LineStyle = LineStyle.Single; + + } + + /// + /// Gets or sets the menu item that opened this menu as a sub-menu. + /// + public MenuItemv2? SuperMenuItem { get; set; } + + /// + protected override void OnVisibleChanged () + { + if (Visible) + { + SelectedMenuItem = SubViews.Where (mi => mi is MenuItemv2).ElementAtOrDefault (0) as MenuItemv2; + } + } + + /// + public override void EndInit () + { + base.EndInit (); + + if (Border is { }) + { + } + } + + /// + protected override void OnSubViewAdded (View view) + { + base.OnSubViewAdded (view); + + if (view is MenuItemv2 menuItem) + { + menuItem.CanFocus = true; + + AddCommand (menuItem.Command, RaiseAccepted); + + menuItem.Selecting += MenuItemOnSelecting; + menuItem.Accepting += MenuItemOnAccepting; + menuItem.Accepted += MenuItemOnAccepted; + + void MenuItemOnSelecting (object? sender, CommandEventArgs e) + { + Logging.Trace ($"Selecting: {e.Context?.Source?.Title}"); + } + + void MenuItemOnAccepting (object? sender, CommandEventArgs e) + { + Logging.Trace ($"Accepting: {e.Context?.Source?.Title}"); + } + + void MenuItemOnAccepted (object? sender, CommandEventArgs e) + { + Logging.Trace ($"Accepted: {e.Context?.Source?.Title}"); + RaiseAccepted (e.Context); + } + } + + if (view is Line line) + { + // Grow line so we get autojoin line + line.X = Pos.Func (() => -Border!.Thickness.Left); + line.Width = Dim.Fill ()! + Dim.Func (() => Border!.Thickness.Right); + } + } + + // TODO: Consider moving Accepted to Bar? + + /// + /// Riases the / event indicating an item in this menu (or submenu) + /// was accepted. This is used to determine when to hide the menu. + /// + /// + /// + protected bool? RaiseAccepted (ICommandContext? ctx) + { + Logging.Trace ($"RaiseAccepted: {ctx}"); + CommandEventArgs args = new () { Context = ctx }; + + OnAccepted (args); + Accepted?.Invoke (this, args); + + return true; + } + + /// + /// Called when the user has accepted an item in this menu (or submenu. This is used to determine when to hide the menu. + /// + /// + /// + /// + protected virtual void OnAccepted (CommandEventArgs args) { } + + /// + /// Raised when the user has accepted an item in this menu (or submenu. This is used to determine when to hide the menu. + /// + /// + /// + /// See for more information. + /// + /// + public event EventHandler? Accepted; + + /// + protected override void OnFocusedChanged (View? previousFocused, View? focused) + { + base.OnFocusedChanged (previousFocused, focused); + SelectedMenuItem = focused as MenuItemv2; + RaiseSelectedMenuItemChanged (SelectedMenuItem); + } + + /// + /// Gets or set the currently selected menu item. This is a helper that + /// tracks . + /// + public MenuItemv2? SelectedMenuItem + { + get => Focused as MenuItemv2; + set + { + if (value == Focused) + { + return; + } + + // Note we DO NOT set focus here; This property tracks Focused + } + } + + internal void RaiseSelectedMenuItemChanged (MenuItemv2? selected) + { + //Logging.Trace ($"RaiseSelectedMenuItemChanged: {selected?.Title}"); + + OnSelectedMenuItemChanged (selected); + SelectedMenuItemChanged?.Invoke (this, selected); + } + + /// + /// Called when the the selected menu item has changed. + /// + /// + protected virtual void OnSelectedMenuItemChanged (MenuItemv2? selected) + { + } + + /// + /// Raised when the selected menu item has changed. + /// + public event EventHandler? SelectedMenuItemChanged; + +} \ No newline at end of file diff --git a/Terminal.Gui/Views/Menu/PopoverMenu.cs b/Terminal.Gui/Views/Menu/PopoverMenu.cs new file mode 100644 index 0000000000..804c1a9a37 --- /dev/null +++ b/Terminal.Gui/Views/Menu/PopoverMenu.cs @@ -0,0 +1,532 @@ +#nullable enable +using System.Diagnostics; + +namespace Terminal.Gui; + +/// +/// Provides a cascading popover menu. +/// +public class PopoverMenu : PopoverBaseImpl +{ + /// + /// Initializes a new instance of the class. + /// + public PopoverMenu () : this (null) { } + + /// + /// Initializes a new instance of the class with the specified root . + /// + public PopoverMenu (Menuv2? root) + { + base.Visible = false; + //base.ColorScheme = Colors.ColorSchemes ["Menu"]; + + Root = root; + + AddCommand (Command.Right, MoveRight); + KeyBindings.Add (Key.CursorRight, Command.Right); + + AddCommand (Command.Left, MoveLeft); + KeyBindings.Add (Key.CursorLeft, Command.Left); + + // TODO: Remove; for debugging for now + AddCommand ( + Command.NotBound, + ctx => + { + Logging.Trace ($"popoverMenu NotBound: {ctx}"); + + return false; + }); + + KeyBindings.Add (DefaultKey, Command.Quit); + KeyBindings.ReplaceCommands (Application.QuitKey, Command.Quit); + + AddCommand ( + Command.Quit, + ctx => + { + if (!Visible) + { + return false; + } + + Visible = false; + + return RaiseAccepted (ctx); + }); + + return; + + bool? MoveLeft (ICommandContext? ctx) + { + if (Focused == Root) + { + return false; + } + + if (MostFocused is MenuItemv2 { SuperView: Menuv2 focusedMenu }) + { + focusedMenu.SuperMenuItem?.SetFocus (); + + return true; + } + + return AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop); + } + + bool? MoveRight (ICommandContext? ctx) + { + if (Focused == Root) + { + return false; + } + + if (MostFocused is MenuItemv2 { SubMenu.Visible: true } focused) + { + focused.SubMenu.SetFocus (); + + return true; + } + + return AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + } + } + + /// + /// The mouse flags that will cause the popover menu to be visible. The default is + /// which is typically the right mouse button. + /// + public static MouseFlags MouseFlags { get; set; } = MouseFlags.Button3Clicked; + + /// The default key for activating popover menus. + [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] + public static Key DefaultKey { get; set; } = Key.F10.WithShift; + + /// + /// Makes the popover menu visible and locates it at . The actual position of the menu + /// will be adjusted to + /// ensure the menu fully fits on the screen, and the mouse cursor is over the first cell of the + /// first MenuItem. + /// + /// If , the current mouse position will be used. + public void MakeVisible (Point? idealScreenPosition = null) + { + UpdateKeyBindings (); + SetPosition (idealScreenPosition); + Application.Popover?.ShowPopover (this); + } + + /// + /// Locates the popover menu at . The actual position of the menu will be adjusted to + /// ensure the menu fully fits on the screen, and the mouse cursor is over the first cell of the + /// first MenuItem (if possible). + /// + /// If , the current mouse position will be used. + public void SetPosition (Point? idealScreenPosition = null) + { + idealScreenPosition ??= Application.GetLastMousePosition (); + + if (idealScreenPosition is { } && Root is { }) + { + Point pos = idealScreenPosition.Value; + + if (!Root.IsInitialized) + { + Root.BeginInit(); + Root.EndInit (); + Root.Layout (); + } + pos = GetMostVisibleLocationForSubMenu (Root, pos); + + Root.X = pos.X; + Root.Y = pos.Y; + } + } + + /// + protected override void OnVisibleChanged () + { + base.OnVisibleChanged (); + + if (Visible) + { + AddAndShowSubMenu (_root); + } + else + { + HideAndRemoveSubMenu (_root); + Application.Popover?.HidePopover (this); + } + } + + private Menuv2? _root; + + /// + /// Gets or sets the that is the root of the Popover Menu. + /// + public Menuv2? Root + { + get => _root; + set + { + if (_root == value) + { + return; + } + + if (_root is { }) + { + _root.Accepting -= MenuOnAccepting; + } + + HideAndRemoveSubMenu (_root); + + _root = value; + + if (_root is { }) + { + _root.Accepting += MenuOnAccepting; + } + + UpdateKeyBindings (); + + IEnumerable allMenus = GetAllSubMenus (); + + foreach (Menuv2 menu in allMenus) + { + menu.Accepting += MenuOnAccepting; + menu.Accepted += MenuAccepted; + menu.SelectedMenuItemChanged += MenuOnSelectedMenuItemChanged; + } + } + } + + private void UpdateKeyBindings () + { + // TODO: This needs to be done whenever any MenuItem in the menu tree changes to support dynamic menus + // TODO: And it needs to clear them first + IEnumerable all = GetMenuItemsOfAllSubMenus (); + + foreach (MenuItemv2 menuItem in all.Where(mi => mi.Command != Command.NotBound)) + { + if (menuItem.TargetView is { }) + { + // A TargetView implies HotKey + // Automatically set MenuItem.Key + Key? key = menuItem.TargetView.HotKeyBindings.GetFirstFromCommands (menuItem.Command); + + if (key is { IsValid: true }) + { + if (menuItem.Key.IsValid) + { + //Logging.Warning ("Do not specify a Key for MenuItems where a Command is specified. Key will be determined automatically."); + } + + menuItem.Key = key; + Logging.Trace ($"HotKey: {menuItem.Key}->{menuItem.Command}"); + } + } + else + { + // No TargetView implies Application HotKey + Key? key = Application.KeyBindings.GetFirstFromCommands (menuItem.Command); + + if (key is { IsValid: true }) + { + if (menuItem.Key.IsValid) + { + // Logging.Warning ("App HotKey: Do not specify a Key for MenuItems where a Command is specified. Key will be determined automatically."); + } + + menuItem.Key = key; + Logging.Trace ($"App HotKey: {menuItem.Key}->{menuItem.Command}"); + } + } + } + + foreach (MenuItemv2 menuItem in all.Where (mi => mi is { Command: Command.NotBound, Key.IsValid: true })) + { + + } + + } + + /// + protected override bool OnKeyDownNotHandled (Key key) + { + // See if any of our MenuItems have this key as Key + IEnumerable all = GetMenuItemsOfAllSubMenus (); + + foreach (MenuItemv2 menuItem in all) + { + if (menuItem.Key == key) + { + return menuItem.NewKeyDownEvent (key); + } + } + + return base.OnKeyDownNotHandled (key); + } + + /// + /// Gets all the submenus in the PopoverMenu. + /// + /// + internal IEnumerable GetAllSubMenus () + { + List result = []; + + if (Root == null) + { + return result; + } + + Stack stack = new (); + stack.Push (Root); + + while (stack.Count > 0) + { + Menuv2 currentMenu = stack.Pop (); + result.Add (currentMenu); + + foreach (View subView in currentMenu.SubViews) + { + if (subView is MenuItemv2 menuItem && menuItem.SubMenu != null) + { + stack.Push (menuItem.SubMenu); + } + } + } + + return result; + } + + /// + /// Gets all the MenuItems in the PopoverMenu. + /// + /// + internal IEnumerable GetMenuItemsOfAllSubMenus () + { + List result = []; + + foreach (Menuv2 menu in GetAllSubMenus ()) + { + foreach (View subView in menu.SubViews) + { + if (subView is MenuItemv2 menuItem) + { + result.Add (menuItem); + } + } + } + + return result; + } + + /// + /// Pops up the submenu of the specified MenuItem, if there is one. + /// + /// + internal void ShowSubMenu (MenuItemv2? menuItem) + { + var menu = menuItem?.SuperView as Menuv2; + + if (menu is { }) + { + menu.Layout (); + } + // If there's a visible peer, remove / hide it + + // Debug.Assert (menu is null || menu?.SubViews.Count (v => v is MenuItemv2 { SubMenu.Visible: true }) < 2); + + if (menu?.SubViews.FirstOrDefault (v => v is MenuItemv2 { SubMenu.Visible: true }) is MenuItemv2 visiblePeer) + { + HideAndRemoveSubMenu (visiblePeer.SubMenu); + visiblePeer.ForceFocusColors = false; + } + + if (menuItem is { SubMenu: { Visible: false } }) + { + AddAndShowSubMenu (menuItem.SubMenu); + + Point idealLocation = ScreenToViewport ( + new ( + menuItem.FrameToScreen ().Right - menuItem.SubMenu.GetAdornmentsThickness ().Left, + menuItem.FrameToScreen ().Top - menuItem.SubMenu.GetAdornmentsThickness ().Top)); + + Point pos = GetMostVisibleLocationForSubMenu (menuItem.SubMenu, idealLocation); + menuItem.SubMenu.X = pos.X; + menuItem.SubMenu.Y = pos.Y; + + menuItem.ForceFocusColors = true; + } + } + + /// + /// Gets the most visible screen-relative location for . + /// + /// The menu to locate. + /// Ideal screen-relative location. + /// + internal Point GetMostVisibleLocationForSubMenu (Menuv2 menu, Point idealLocation) + { + var pos = Point.Empty; + + // Calculate the initial position to the right of the menu item + GetLocationEnsuringFullVisibility ( + menu, + idealLocation.X, + idealLocation.Y, + out int nx, + out int ny); + + return new (nx, ny); + } + + private void AddAndShowSubMenu (Menuv2? menu) + { + if (menu is { SuperView: null }) + { + // TODO: Find the menu item below the mouse, if any, and select it + + // TODO: Enable No Border menu style + menu.Border.LineStyle = LineStyle.Single; + menu.Border.Thickness = new (1); + + if (!menu.IsInitialized) + { + menu.BeginInit (); + menu.EndInit (); + } + + menu.ClearFocus (); + base.Add (menu); + + + // IMPORTANT: This must be done after adding the menu to the super view or Add will try + // to set focus to it. + menu.Visible = true; + + menu.Layout (); + } + } + + private void HideAndRemoveSubMenu (Menuv2? menu) + { + if (menu is { Visible: true }) + { + // If there's a visible submenu, remove / hide it + // Debug.Assert (menu.SubViews.Count (v => v is MenuItemv2 { SubMenu.Visible: true }) <= 1); + + if (menu.SubViews.FirstOrDefault (v => v is MenuItemv2 { SubMenu.Visible: true }) is MenuItemv2 visiblePeer) + { + HideAndRemoveSubMenu (visiblePeer.SubMenu); + visiblePeer.ForceFocusColors = false; + } + + menu.Visible = false; + menu.ClearFocus (); + base.Remove (menu); + + if (menu == Root) + { + Visible = false; + } + } + } + + private void MenuOnAccepting (object? sender, CommandEventArgs e) + { + if (e.Context?.Command != Command.HotKey) + { + Visible = false; + } + else + { + // This supports the case when a hotkey of a menuitem with a submenu is pressed + e.Cancel = true; + } + + Logging.Trace ($"{e.Context?.Source?.Title}"); + } + + private void MenuAccepted (object? sender, CommandEventArgs e) + { + Logging.Trace ($"{e.Context?.Source?.Title}"); + + if (e.Context?.Source is MenuItemv2 { SubMenu: null }) + { + HideAndRemoveSubMenu (_root); + RaiseAccepted (e.Context); + } + else if (e.Context?.Source is MenuItemv2 { SubMenu: { } } menuItemWithSubMenu) + { + ShowSubMenu (menuItemWithSubMenu); + } + } + + /// + /// Riases the / event indicating a menu (or submenu) + /// was accepted and the Menus in the PopoverMenu were hidden. Use this to determine when to hide the PopoverMenu. + /// + /// + /// + protected bool? RaiseAccepted (ICommandContext? ctx) + { + Logging.Trace ($"RaiseAccepted: {ctx}"); + CommandEventArgs args = new () { Context = ctx }; + + OnAccepted (args); + Accepted?.Invoke (this, args); + + return true; + } + + /// + /// Called when the user has accepted an item in this menu (or submenu. This is used to determine when to hide the + /// menu. + /// + /// + /// + /// + protected virtual void OnAccepted (CommandEventArgs args) { } + + /// + /// Raised when the user has accepted an item in this menu (or submenu. This is used to determine when to hide the + /// menu. + /// + /// + /// + /// See for more information. + /// + /// + public event EventHandler? Accepted; + + private void MenuOnSelectedMenuItemChanged (object? sender, MenuItemv2? e) + { + //Logging.Trace ($"{e}"); + ShowSubMenu (e); + } + + /// + protected override void Dispose (bool disposing) + { + if (disposing) + { + IEnumerable allMenus = GetAllSubMenus (); + + foreach (Menuv2 menu in allMenus) + { + menu.Accepting -= MenuOnAccepting; + menu.Accepted -= MenuAccepted; + menu.SelectedMenuItemChanged -= MenuOnSelectedMenuItemChanged; + } + + _root?.Dispose (); + _root = null; + } + + base.Dispose (disposing); + } +} diff --git a/Terminal.Gui/Views/MenuBarv2.cs b/Terminal.Gui/Views/MenuBarv2.cs deleted file mode 100644 index 4f1434c348..0000000000 --- a/Terminal.Gui/Views/MenuBarv2.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Reflection; - -namespace Terminal.Gui; - -/// -/// A menu bar is a that snaps to the top of a displaying set of -/// s. -/// -public class MenuBarv2 : Bar -{ - /// - public MenuBarv2 () : this ([]) { } - - /// - public MenuBarv2 (IEnumerable shortcuts) : base (shortcuts) - { - Y = 0; - Width = Dim.Fill (); - Height = Dim.Auto (DimAutoStyle.Content, 1); - BorderStyle = LineStyle.Dashed; - ColorScheme = Colors.ColorSchemes ["Menu"]; - Orientation = Orientation.Horizontal; - - SubViewLayout += MenuBarv2_LayoutStarted; - } - - // MenuBarv2 arranges the items horizontally. - // The first item has no left border, the last item has no right border. - // The Shortcuts are configured with the command, help, and key views aligned in reverse order (EndToStart). - private void MenuBarv2_LayoutStarted (object sender, LayoutEventArgs e) - { - - } - - /// - protected override void OnSubViewAdded (View subView) - { - subView.CanFocus = false; - - if (subView is Shortcut shortcut) - { - // TODO: not happy about using AlignmentModes for this. Too implied. - // TODO: instead, add a property (a style enum?) to Shortcut to control this - //shortcut.AlignmentModes = AlignmentModes.EndToStart; - - shortcut.KeyView.Visible = false; - shortcut.HelpView.Visible = false; - } - } -} diff --git a/Terminal.Gui/Views/Menuv2.cs b/Terminal.Gui/Views/Menuv2.cs deleted file mode 100644 index e9d85ed41a..0000000000 --- a/Terminal.Gui/Views/Menuv2.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.ComponentModel; -using System.Reflection; - -namespace Terminal.Gui; - -/// -/// -public class Menuv2 : Bar -{ - /// - public Menuv2 () : this ([]) { } - - /// - public Menuv2 (IEnumerable shortcuts) : base (shortcuts) - { - Orientation = Orientation.Vertical; - Width = Dim.Auto (); - Height = Dim.Auto (DimAutoStyle.Content, 1); - Initialized += Menuv2_Initialized; - VisibleChanged += OnVisibleChanged; - } - - private void OnVisibleChanged (object sender, EventArgs e) - { - if (Visible) - { - //Application.GrabMouse(this); - } - else - { - if (Application.MouseGrabView == this) - { - //Application.UngrabMouse (); - } - } - } - - private void Menuv2_Initialized (object sender, EventArgs e) - { - Border.Thickness = new Thickness (1, 1, 1, 1); - Border.LineStyle = LineStyle.Single; - ColorScheme = Colors.ColorSchemes ["Menu"]; - } - - // Menuv2 arranges the items horizontally. - // The first item has no left border, the last item has no right border. - // The Shortcuts are configured with the command, help, and key views aligned in reverse order (EndToStart). - /// - protected override void OnSubViewLayout (LayoutEventArgs args) - { - for (int index = 0; index < SubViews.Count; index++) - { - View barItem = SubViews.ElementAt (index); - - if (!barItem.Visible) - { - continue; - } - - } - base.OnSubViewLayout (args); - } - - /// - /// - protected override void OnSubViewAdded (View subView) - { - if (subView is Shortcut shortcut) - { - shortcut.CanFocus = true; - shortcut.Orientation = Orientation.Vertical; - shortcut.HighlightStyle |= HighlightStyle.Hover; - - // TODO: not happy about using AlignmentModes for this. Too implied. - // TODO: instead, add a property (a style enum?) to Shortcut to control this - //shortcut.AlignmentModes = AlignmentModes.EndToStart; - - shortcut.Accepting += ShortcutOnAccept; - - void ShortcutOnAccept (object sender, CommandEventArgs e) - { - if (Arrangement.HasFlag (ViewArrangement.Overlapped) && Visible) - { - Visible = false; - e.Cancel = true; - - return; - } - - //if (!e.Handled) - //{ - // RaiseAcceptEvent (); - //} - } - } - } -} diff --git a/Terminal.Gui/Views/MessageBox.cs b/Terminal.Gui/Views/MessageBox.cs index 31b4a41ec2..f7b2979f51 100644 --- a/Terminal.Gui/Views/MessageBox.cs +++ b/Terminal.Gui/Views/MessageBox.cs @@ -360,26 +360,20 @@ params string [] buttons b.IsDefault = true; b.Accepting += (_, e) => { - if (e.Context is not CommandContext keyCommandContext) - { - return; - } - - // TODO: With https://github.com/gui-cs/Terminal.Gui/issues/3778 we can simplify this - if (keyCommandContext.Binding.Data is Button button) + if (e?.Context?.Source is Button button) { Clicked = (int)button.Data!; } - else if (keyCommandContext.Binding.Target is Button btn) - { - Clicked = (int)btn.Data!; - } else { Clicked = defaultButton; } - e.Cancel = true; + if (e is { }) + { + e.Cancel = true; + } + Application.RequestStop (); }; } diff --git a/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs b/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs index 78682dcfb7..28f02fdcfc 100644 --- a/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs +++ b/Terminal.Gui/Views/ScrollBar/ScrollSlider.cs @@ -241,7 +241,7 @@ private void RaisePositionChangeEvents (int newPosition) OnScrolled (distance); Scrolled?.Invoke (this, new (in distance)); - RaiseSelecting (new CommandContext (Command.Select, new KeyBinding ([Command.Select], null, distance))); + RaiseSelecting (new CommandContext (Command.Select, this, new KeyBinding ([Command.Select], null, distance))); } /// diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index 342d544563..11f4e51754 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -46,38 +46,6 @@ public class Shortcut : View, IOrientation, IDesignable /// public Shortcut () : this (Key.Empty, null, null, null) { } - /// - /// Creates a new instance of , binding it to and - /// . The Key - /// has bound to will be used as . - /// - /// - /// - /// This is a helper API that simplifies creation of multiple Shortcuts when adding them to -based - /// objects, like . - /// - /// - /// - /// The View that will be invoked on when user does something that causes the Shortcut's Accept - /// event to be raised. - /// - /// - /// The Command to invoke on . The Key - /// has bound to will be used as - /// - /// The text to display for the command. - /// The help text to display. - public Shortcut (View targetView, Command command, string commandText, string? helpText = null) - : this ( - targetView?.HotKeyBindings.GetFirstFromCommands (command)!, - commandText, - null, - helpText) - { - _targetView = targetView; - Command = command; - } - /// /// Creates a new instance of . /// @@ -132,11 +100,12 @@ public Shortcut (Key key, string? commandText, Action? action, string? helpText Action = action; - SubViewLayout += OnLayoutStarted; - ShowHide (); } + /// + protected override bool OnClearingViewport () { return base.OnClearingViewport (); } + // Helper to set Width consistently internal Dim GetWidthDimAuto () { @@ -158,10 +127,11 @@ protected override bool OnHighlight (CancelEventArgs args) { if (args.NewValue.HasFlag (HighlightStyle.Hover)) { - HasFocus = true; + SetFocus (); + return true; } - return true; + return false; } /// @@ -204,13 +174,14 @@ internal void ShowHide () SetHelpViewDefaultLayout (); } - if (KeyView.Visible && Key != Key.Empty) + if (KeyView.Visible && (Key != Key.Empty || KeyView.Text != string.Empty)) { Add (KeyView); SetKeyViewDefaultLayout (); } - SetColors (); + // BUGBUG: Causes ever other layout to lose focus colors + //SetColors (); } // Force Width to DimAuto to calculate natural width and then set it back @@ -234,8 +205,11 @@ private Thickness GetMarginThickness () } // When layout starts, we need to adjust the layout of the HelpView and KeyView - private void OnLayoutStarted (object? sender, LayoutEventArgs e) + /// + protected override void OnSubViewLayout (LayoutEventArgs e) { + base.OnSubViewLayout (e); + ShowHide (); ForceCalculateNaturalWidth (); @@ -278,18 +252,6 @@ private void OnLayoutStarted (object? sender, LayoutEventArgs e) #region Accept/Select/HotKey Command Handling - private readonly View? _targetView; // If set, _command will be invoked - - /// - /// Gets the target that the will be invoked on. - /// - public View? TargetView => _targetView; - - /// - /// Gets the that will be invoked on when the Shortcut is activated. - /// - public Command Command { get; } - private void AddCommands () { // Accept (Enter key) - @@ -300,18 +262,24 @@ private void AddCommands () AddCommand (Command.Select, DispatchCommand); } - private bool? DispatchCommand (ICommandContext? commandContext) + /// + /// Called when a Command has been invoked on this Shortcut. + /// + /// + /// + internal virtual bool? DispatchCommand (ICommandContext? commandContext) { - CommandContext? keyCommandContext = commandContext is CommandContext ? (CommandContext)commandContext : default; + CommandContext? keyCommandContext = commandContext as CommandContext? ?? default (CommandContext); if (keyCommandContext?.Binding.Data != this) { - // Invoke Select on the command view to cause it to change state if it wants to + // Invoke Select on the CommandView to cause it to change state if it wants to // If this causes CommandView to raise Accept, we eat it keyCommandContext = keyCommandContext!.Value with { Binding = keyCommandContext.Value.Binding with { Data = this } }; CommandView.InvokeCommand (Command.Select, keyCommandContext); } + // BUGBUG: Why does this use keyCommandContext and not commandContext? if (RaiseSelecting (keyCommandContext) is true) { return true; @@ -322,6 +290,10 @@ private void AddCommands () var cancel = false; + if (commandContext is { }) + { + commandContext.Source = this; + } cancel = RaiseAccepting (commandContext) is true; if (cancel) @@ -342,10 +314,6 @@ private void AddCommands () cancel = true; } - if (_targetView is { }) - { - _targetView.InvokeCommand (Command, commandContext); - } return cancel; } @@ -502,7 +470,6 @@ void CommandViewOnSelecting (object? sender, CommandEventArgs e) InvokeCommand (Command.Select, new ([Command.Select], null, this)); } - // BUGBUG: This prevents NumericUpDown on statusbar in HexEditor from working e.Cancel = true; } } @@ -668,12 +635,6 @@ public int MinimumKeyTextSize _minimumKeyTextSize = value; SetKeyViewDefaultLayout (); - - //// TODO: Prob not needed - //CommandView.SetNeedsLayout (); - //HelpView.SetNeedsLayout (); - //KeyView.SetNeedsLayout (); - //SetSubViewNeedsDraw (); } } @@ -700,28 +661,30 @@ private void SetKeyViewDefaultLayout () private void UpdateKeyBindings (Key oldKey) { - if (Key.IsValid) + if (!Key.IsValid) { - if (BindKeyToApplication) - { - if (oldKey != Key.Empty) - { - Application.KeyBindings.Remove (oldKey); - } + return; + } - Application.KeyBindings.Remove (Key); - Application.KeyBindings.Add (Key, this, Command.HotKey); - } - else + if (BindKeyToApplication) + { + if (oldKey != Key.Empty) { - if (oldKey != Key.Empty) - { - HotKeyBindings.Remove (oldKey); - } + Application.KeyBindings.Remove (oldKey); + } - HotKeyBindings.Remove (Key); - HotKeyBindings.Add (Key, Command.HotKey); + Application.KeyBindings.Remove (Key); + Application.KeyBindings.Add (Key, this, Command.HotKey); + } + else + { + if (oldKey != Key.Empty) + { + HotKeyBindings.Remove (oldKey); } + + HotKeyBindings.Remove (Key); + HotKeyBindings.Add (Key, Command.HotKey); } } @@ -740,12 +703,29 @@ public override ColorScheme? ColorScheme } } + private bool _forceFocusColors; + + /// + /// TODO: IS this needed? + /// + public bool ForceFocusColors + { + get => _forceFocusColors; + set + { + _forceFocusColors = value; + SetColors (value); + //SetNeedsDraw(); + } + } + private ColorScheme? _nonFocusColorScheme; + /// /// internal void SetColors (bool highlight = false) { - if (HasFocus || highlight) + if (HasFocus || highlight || ForceFocusColors) { if (_nonFocusColorScheme is null) { @@ -757,10 +737,10 @@ internal void SetColors (bool highlight = false) // When we have focus, we invert the colors base.ColorScheme = new (base.ColorScheme) { - Normal = base.ColorScheme.Focus, - HotNormal = base.ColorScheme.HotFocus, - HotFocus = base.ColorScheme.HotNormal, - Focus = base.ColorScheme.Normal + Normal = GetFocusColor (), + HotNormal = GetHotFocusColor (), + HotFocus = GetHotNormalColor (), + Focus = GetNormalColor (), }; } else @@ -781,8 +761,8 @@ internal void SetColors (bool highlight = false) { var cs = new ColorScheme (base.ColorScheme) { - Normal = base.ColorScheme.HotNormal, - HotNormal = base.ColorScheme.Normal + Normal = GetHotNormalColor (), + HotNormal = GetNormalColor () }; KeyView.ColorScheme = cs; } @@ -803,7 +783,10 @@ internal void SetColors (bool highlight = false) } /// - protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? view) { SetColors (); } + protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? view) + { + SetColors (); + } #endregion Focus diff --git a/Terminal.Gui/Views/Slider.cs b/Terminal.Gui/Views/Slider.cs index a0cc6b3356..ff7ea9b70d 100644 --- a/Terminal.Gui/Views/Slider.cs +++ b/Terminal.Gui/Views/Slider.cs @@ -841,7 +841,7 @@ private string AlignText (string text, int width, Alignment alignment) private void DrawSlider () { // TODO: be more surgical on clear - ClearViewport (); + ClearViewport (null); // Attributes diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index 6c872d95c4..604e34e28d 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -316,7 +316,7 @@ public TextField () Command.Context, () => { - ShowContextMenu (); + ShowContextMenu (keyboard: true); return true; } @@ -395,14 +395,12 @@ public TextField () KeyBindings.Add (Key.R.WithCtrl, Command.DeleteAll); KeyBindings.Add (Key.D.WithCtrl.WithShift, Command.DeleteAll); - _currentCulture = Thread.CurrentThread.CurrentUICulture; + KeyBindings.Remove (Key.Space); - ContextMenu = new () { Host = this }; - ContextMenu.KeyChanged += ContextMenu_KeyChanged; + _currentCulture = Thread.CurrentThread.CurrentUICulture; + CreateContextMenu (); KeyBindings.Add (ContextMenu.Key, Command.Context); - - KeyBindings.Remove (Key.Space); } /// @@ -421,7 +419,8 @@ public TextField () public Color CaptionColor { get; set; } /// Get the for this view. - public ContextMenu ContextMenu { get; } + [CanBeNull] + public ContextMenuv2 ContextMenu { get; private set; } /// Sets or gets the current cursor position. public virtual int CursorPosition @@ -801,7 +800,7 @@ protected override bool OnMouseEvent (MouseEventArgs ev) && !ev.Flags.HasFlag (MouseFlags.ReportMousePosition) && !ev.Flags.HasFlag (MouseFlags.Button1DoubleClicked) && !ev.Flags.HasFlag (MouseFlags.Button1TripleClicked) - && !ev.Flags.HasFlag (ContextMenu.MouseFlags)) + && !ev.Flags.HasFlag (PopoverMenu.MouseFlags)) { return false; } @@ -901,9 +900,10 @@ protected override bool OnMouseEvent (MouseEventArgs ev) ClearAllSelection (); PrepareSelection (0, _text.Count); } - else if (ev.Flags == ContextMenu.MouseFlags) + else if (ev.Flags == PopoverMenu.MouseFlags) { - ShowContextMenu (); + PositionCursor (ev); + ShowContextMenu (false); } //SetNeedsDraw (); @@ -1223,72 +1223,31 @@ private void Adjust () } } - private MenuBarItem BuildContextMenuBarItem () + private void CreateContextMenu () { - return new ( - new MenuItem [] - { - new ( - Strings.ctxSelectAll, - "", - () => SelectAll (), - null, - null, - (KeyCode)KeyBindings.GetFirstFromCommands (Command.SelectAll) - ), - new ( - Strings.ctxDeleteAll, - "", - () => DeleteAll (), - null, - null, - (KeyCode)KeyBindings.GetFirstFromCommands (Command.DeleteAll) - ), - new ( - Strings.ctxCopy, - "", - () => Copy (), - null, - null, - (KeyCode)KeyBindings.GetFirstFromCommands (Command.Copy) - ), - new ( - Strings.ctxCut, - "", - () => Cut (), - null, - null, - (KeyCode)KeyBindings.GetFirstFromCommands (Command.Cut) - ), - new ( - Strings.ctxPaste, - "", - () => Paste (), - null, - null, - (KeyCode)KeyBindings.GetFirstFromCommands (Command.Paste) - ), - new ( - Strings.ctxUndo, - "", - () => Undo (), - null, - null, - (KeyCode)KeyBindings.GetFirstFromCommands (Command.Undo) - ), - new ( - Strings.ctxRedo, - "", - () => Redo (), - null, - null, - (KeyCode)KeyBindings.GetFirstFromCommands (Command.Redo) - ) - } - ); + DisposeContextMenu (); + ContextMenuv2 menu = new (new List () + { + new (this, Command.SelectAll, Strings.ctxSelectAll), + new (this, Command.DeleteAll, Strings.ctxDeleteAll), + new (this, Command.Copy, Strings.ctxCopy), + new (this, Command.Cut, Strings.ctxCut), + new (this, Command.Paste, Strings.ctxPaste), + new (this, Command.Undo, Strings.ctxUndo), + new (this, Command.Redo, Strings.ctxRedo), + }); + + HotKeyBindings.Remove (menu.Key); + HotKeyBindings.Add (menu.Key, Command.Context); + menu.KeyChanged += ContextMenu_KeyChanged; + + ContextMenu = menu; } - private void ContextMenu_KeyChanged (object sender, KeyChangedEventArgs e) { KeyBindings.Replace (e.OldKey.KeyCode, e.NewKey.KeyCode); } + private void ContextMenu_KeyChanged (object sender, KeyChangedEventArgs e) + { + KeyBindings.Replace (e.OldKey.KeyCode, e.NewKey.KeyCode); + } private List DeleteSelectedText () { @@ -1808,14 +1767,27 @@ private void SetSelectedStartSelectedLength () private void SetText (List newText) { Text = StringExtensions.ToString (newText); } private void SetText (IEnumerable newText) { SetText (newText.ToList ()); } - private void ShowContextMenu () + private void ShowContextMenu (bool keyboard) { + if (!Equals (_currentCulture, Thread.CurrentThread.CurrentUICulture)) { _currentCulture = Thread.CurrentThread.CurrentUICulture; + + if (ContextMenu is { }) + { + CreateContextMenu (); + } } - ContextMenu.Show (BuildContextMenuBarItem ()); + if (keyboard) + { + ContextMenu?.MakeVisible(ViewportToScreen (new Point (_cursorPosition - ScrollOffset, 1))); + } + else + { + ContextMenu?.MakeVisible (); + } } private void TextField_SuperViewChanged (object sender, SuperViewChangedEventArgs e) @@ -1849,6 +1821,27 @@ private void TextField_Initialized (object sender, EventArgs e) Autocomplete.PopupInsideContainer = false; } } + + private void DisposeContextMenu () + { + if (ContextMenu is { }) + { + ContextMenu.Visible = false; + ContextMenu.KeyChanged -= ContextMenu_KeyChanged; + ContextMenu.Dispose (); + ContextMenu = null; + } + } + + /// + protected override void Dispose (bool disposing) + { + if (disposing) + { + DisposeContextMenu (); + } + base.Dispose (disposing); + } } /// diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index af0d74226c..f89c6aeab8 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -2290,11 +2290,7 @@ public TextView () Command.Context, () => { - ContextMenu!.Position = new ( - CursorPosition.X - _leftColumn + 2, - CursorPosition.Y - _topRow + 2 - ); - ShowContextMenu (); + ShowContextMenu (true); return true; } @@ -2410,9 +2406,7 @@ public TextView () _currentCulture = Thread.CurrentThread.CurrentUICulture; - ContextMenu = new (); - ContextMenu.KeyChanged += ContextMenu_KeyChanged!; - + ContextMenu = CreateContextMenu (); KeyBindings.Add (ContextMenu.Key, Command.Context); } @@ -2496,8 +2490,8 @@ public bool AllowsTab /// public IAutocomplete Autocomplete { get; protected set; } = new TextViewAutocomplete (); - /// Get the for this view. - public ContextMenu? ContextMenu { get; } + /// Get the for this view. + public ContextMenuv2? ContextMenu { get; private set; } /// Gets the cursor column. /// The cursor column. @@ -3505,8 +3499,12 @@ protected override bool OnMouseEvent (MouseEventArgs ev) } else if (ev.Flags == ContextMenu!.MouseFlags) { - ContextMenu.Position = ViewportToScreen ((Viewport with { X = ev.Position.X, Y = ev.Position.Y }).Location); - ShowContextMenu (); + ContextMenu!.X = ev.ScreenPosition.X; + ContextMenu!.Y = ev.ScreenPosition.Y; + + ShowContextMenu (false); + //ContextMenu.Position = ViewportToScreen ((Viewport with { X = ev.Position.X, Y = ev.Position.Y }).Location); + //ShowContextMenu (); } return true; @@ -4150,77 +4148,22 @@ private void Adjust () private void AppendClipboard (string text) { Clipboard.Contents += text; } - private MenuBarItem? BuildContextMenuBarItem () + private ContextMenuv2 CreateContextMenu () { - return new ( - new MenuItem [] + ContextMenuv2 menu = new (new List () { - new ( - Strings.ctxSelectAll, - "", - SelectAll, - null, - null, - (KeyCode)KeyBindings.GetFirstFromCommands (Command.SelectAll) - ), - new ( - Strings.ctxDeleteAll, - "", - DeleteAll, - null, - null, - (KeyCode)KeyBindings.GetFirstFromCommands (Command.DeleteAll) - ), - new ( - Strings.ctxCopy, - "", - Copy, - null, - null, - (KeyCode)KeyBindings.GetFirstFromCommands (Command.Copy) - ), - new ( - Strings.ctxCut, - "", - Cut, - null, - null, - (KeyCode)KeyBindings.GetFirstFromCommands (Command.Cut) - ), - new ( - Strings.ctxPaste, - "", - Paste, - null, - null, - (KeyCode)KeyBindings.GetFirstFromCommands (Command.Paste) - ), - new ( - Strings.ctxUndo, - "", - Undo, - null, - null, - (KeyCode)KeyBindings.GetFirstFromCommands (Command.Undo) - ), - new ( - Strings.ctxRedo, - "", - Redo, - null, - null, - (KeyCode)KeyBindings.GetFirstFromCommands (Command.Redo) - ), - new ( - Strings.ctxColors, - "", - () => PromptForColors (), - null, - null, - (KeyCode)KeyBindings.GetFirstFromCommands (Command.Open) - ) - } - ); + new (this, Command.SelectAll, Strings.ctxSelectAll), + new (this, Command.DeleteAll, Strings.ctxDeleteAll), + new (this, Command.Copy, Strings.ctxCopy), + new (this, Command.Cut, Strings.ctxCut), + new (this, Command.Paste, Strings.ctxPaste), + new (this, Command.Undo, Strings.ctxUndo), + new (this, Command.Redo, Strings.ctxRedo), + }); + + menu.KeyChanged += ContextMenu_KeyChanged; + + return menu; } private void ClearRegion (int left, int top, int right, int bottom) @@ -4331,7 +4274,7 @@ private void ClearSelectedRegion () DoNeededAction (); } - private void ContextMenu_KeyChanged (object sender, KeyChangedEventArgs e) { KeyBindings.Replace (e.OldKey, e.NewKey); } + private void ContextMenu_KeyChanged (object? sender, KeyChangedEventArgs e) { KeyBindings.Replace (e.OldKey, e.NewKey); } private bool DeleteTextBackwards () { @@ -6387,14 +6330,14 @@ private void SetWrapModel ([CallerMemberName] string? caller = null) } } - private void ShowContextMenu () + private void ShowContextMenu (bool keyboard) { if (!Equals (_currentCulture, Thread.CurrentThread.CurrentUICulture)) { _currentCulture = Thread.CurrentThread.CurrentUICulture; } - ContextMenu!.Show (BuildContextMenuBarItem ()); + ContextMenu?.MakeVisible(ViewportToScreen(new Point (CursorPosition.X, CursorPosition.Y))); } private void StartSelecting () @@ -6567,6 +6510,18 @@ private void WrapTextModel () SetNeedsDraw (); } } + + /// + protected override void Dispose (bool disposing) + { + if (disposing && ContextMenu is { }) + { + ContextMenu.Visible = false; + ContextMenu.Dispose (); + ContextMenu = null; + } + base.Dispose (disposing); + } } /// diff --git a/Tests/UnitTests/Application/ApplicationPopoverTests.cs b/Tests/UnitTests/Application/ApplicationPopoverTests.cs new file mode 100644 index 0000000000..20ca401082 --- /dev/null +++ b/Tests/UnitTests/Application/ApplicationPopoverTests.cs @@ -0,0 +1,444 @@ +using static System.Net.Mime.MediaTypeNames; + +namespace Terminal.Gui.ApplicationTests; + +public class ApplicationPopoverTests +{ + [Fact] + public void Popover_ApplicationInit_Inits () + { + // Arrange + Assert.Null (Application.Popover); + Application.Init (new FakeDriver ()); + + // Act + Assert.NotNull (Application.Popover); + + Application.ResetState (true); + } + + [Fact] + public void Popover_ApplicationShutdown_CleansUp () + { + // Arrange + Assert.Null (Application.Popover); + Application.Init (new FakeDriver ()); + + // Act + Assert.NotNull (Application.Popover); + + Application.Shutdown (); + + // Test + Assert.Null (Application.Popover); + } + + [Fact] + public void Popover_NotCleanedUp_On_End () + { + // Arrange + Assert.Null (Application.Popover); + Application.Init (new FakeDriver ()); + Assert.NotNull (Application.Popover); + Application.Iteration += (s, a) => Application.RequestStop (); + + var top = new Toplevel (); + RunState rs = Application.Begin (top); + + // Act + Application.End (rs); + + // Test + Assert.NotNull (Application.Popover); + + top.Dispose (); + Application.Shutdown (); + } + + [Fact] + public void Popover_Active_Hidden_On_End () + { + // Arrange + Assert.Null (Application.Popover); + Application.Init (new FakeDriver ()); + Application.Iteration += (s, a) => Application.RequestStop (); + + var top = new Toplevel (); + RunState rs = Application.Begin (top); + + IPopoverTestClass popover = new (); + + Application.Popover?.ShowPopover (popover); + Assert.True (popover.Visible); + + // Act + Application.End (rs); + top.Dispose (); + + // Test + Assert.False (popover.Visible); + Assert.NotNull (Application.Popover); + + popover.Dispose (); + Application.Shutdown (); + } + + public class IPopoverTestClass : View, IPopover + { + public List HandledKeys { get; } = new List (); + public int NewCommandInvokeCount { get; private set; } + + public IPopoverTestClass () + { + CanFocus = true; + AddCommand (Command.New, NewCommandHandler); + HotKeyBindings.Add (Key.N.WithCtrl, Command.New); + + bool? NewCommandHandler (ICommandContext ctx) + { + NewCommandInvokeCount++; + + return false; + } + } + + protected override bool OnKeyDown (Key key) + { + HandledKeys.Add (key); + return false; + } + } + //[Fact] + //public void Popover_SetToNull () + //{ + // // Arrange + // var popover = new View (); + // Application.Popover = popover; + + // // Act + // Application.Popover = null; + + // // Assert + // Assert.Null (Application.Popover); + + // Application.ResetState (ignoreDisposed: true); + //} + + //[Fact] + //public void Popover_VisibleChangedEvent () + //{ + // // Arrange + // var popover = new View () + // { + // Visible = false + // }; + // Application.Popover = popover; + // bool eventTriggered = false; + + // popover.VisibleChanged += (sender, e) => eventTriggered = true; + + // // Act + // popover.Visible = true; + + // // Assert + // Assert.True (eventTriggered); + + // Application.ResetState (ignoreDisposed: true); + //} + + //[Fact] + //public void Popover_InitializesCorrectly () + //{ + // // Arrange + // var popover = new View (); + + // // Act + // Application.Popover = popover; + + // // Assert + // Assert.True (popover.IsInitialized); + + // Application.ResetState (ignoreDisposed: true); + //} + + //[Fact] + //public void Popover_SetsColorScheme () + //{ + // // Arrange + // var popover = new View (); + // var topColorScheme = new ColorScheme (); + // Application.Top = new Toplevel { ColorScheme = topColorScheme }; + + // // Act + // Application.Popover = popover; + + // // Assert + // Assert.Equal (topColorScheme, popover.ColorScheme); + + // Application.ResetState (ignoreDisposed: true); + //} + + //[Fact] + //public void Popover_VisibleChangedToTrue_SetsFocus () + //{ + // // Arrange + // var popover = new View () + // { + // Visible = false, + // CanFocus = true + // }; + // Application.Popover = popover; + + // // Act + // popover.Visible = true; + + // // Assert + // Assert.True (popover.Visible); + // Assert.True (popover.HasFocus); + + // Application.ResetState (ignoreDisposed: true); + //} + + //[Theory] + //[InlineData(-1, -1)] + //[InlineData (0, 0)] + //[InlineData (2048, 2048)] + //[InlineData (2049, 2049)] + //public void Popover_VisibleChangedToTrue_Locates_In_Visible_Position (int x, int y) + //{ + // // Arrange + // var popover = new View () + // { + // X = x, + // Y = y, + // Visible = false, + // CanFocus = true, + // Width = 1, + // Height = 1 + // }; + // Application.Popover = popover; + + // // Act + // popover.Visible = true; + // Application.LayoutAndDraw(); + + // // Assert + // Assert.True (Application.Screen.Contains (popover.Frame)); + + // Application.ResetState (ignoreDisposed: true); + //} + + //[Fact] + //public void Popover_VisibleChangedToFalse_Hides_And_Removes_Focus () + //{ + // // Arrange + // var popover = new View () + // { + // Visible = false, + // CanFocus = true + // }; + // Application.Popover = popover; + // popover.Visible = true; + + // // Act + // popover.Visible = false; + + // // Assert + // Assert.False (popover.Visible); + // Assert.False (popover.HasFocus); + + // Application.ResetState (ignoreDisposed: true); + //} + + //[Fact] + //public void Popover_Quit_Command_Hides () + //{ + // // Arrange + // var popover = new View () + // { + // Visible = false, + // CanFocus = true + // }; + // Application.Popover = popover; + // popover.Visible = true; + // Assert.True (popover.Visible); + // Assert.True (popover.HasFocus); + + // // Act + // Application.RaiseKeyDownEvent (Application.QuitKey); + + // // Assert + // Assert.False (popover.Visible); + // Assert.False (popover.HasFocus); + + // Application.ResetState (ignoreDisposed: true); + //} + + //[Fact] + //public void Popover_MouseClick_Outside_Hides_Passes_Event_On () + //{ + // // Arrange + // Application.Top = new Toplevel () + // { + // Id = "top", + // Height = 10, + // Width = 10, + // }; + + // View otherView = new () + // { + // X = 1, + // Y = 1, + // Height = 1, + // Width = 1, + // Id = "otherView", + // }; + + // bool otherViewPressed = false; + // otherView.MouseEvent += (sender, e) => + // { + // otherViewPressed = e.Flags.HasFlag(MouseFlags.Button1Pressed); + // }; + + // Application.Top.Add (otherView); + + // var popover = new View () + // { + // Id = "popover", + // X = 5, + // Y = 5, + // Width = 1, + // Height = 1, + // Visible = false, + // CanFocus = true + // }; + + // Application.Popover = popover; + // popover.Visible = true; + // Assert.True (popover.Visible); + // Assert.True (popover.HasFocus); + + // // Act + // // Click on popover + // Application.RaiseMouseEvent (new () { Flags = MouseFlags.Button1Pressed, ScreenPosition = new (5, 5) }); + // Assert.True (popover.Visible); + + // // Click outside popover (on button) + // Application.RaiseMouseEvent (new () { Flags = MouseFlags.Button1Pressed, ScreenPosition = new (1, 1) }); + + // // Assert + // Assert.True (otherViewPressed); + // Assert.False (popover.Visible); + + // Application.Top.Dispose (); + // Application.ResetState (ignoreDisposed: true); + //} + + //[Theory] + //[InlineData (0, 0, false)] + //[InlineData (5, 5, true)] + //[InlineData (10, 10, false)] + //[InlineData (5, 10, false)] + //[InlineData (9, 9, false)] + //public void Popover_MouseClick_Outside_Hides (int mouseX, int mouseY, bool expectedVisible) + //{ + // // Arrange + // Application.Top = new Toplevel () + // { + // Id = "top", + // Height = 10, + // Width = 10, + // }; + // var popover = new View () + // { + // Id = "popover", + // X = 5, + // Y = 5, + // Width = 1, + // Height = 1, + // Visible = false, + // CanFocus = true + // }; + + // Application.Popover = popover; + // popover.Visible = true; + // Assert.True (popover.Visible); + // Assert.True (popover.HasFocus); + + // // Act + // Application.RaiseMouseEvent (new () { Flags = MouseFlags.Button1Pressed, ScreenPosition = new (mouseX, mouseY) }); + + // // Assert + // Assert.Equal (expectedVisible, popover.Visible); + + // Application.Top.Dispose (); + // Application.ResetState (ignoreDisposed: true); + //} + + //[Fact] + //public void Popover_SetAndGet_ReturnsCorrectValue () + //{ + // // Arrange + // var view = new View (); + + // // Act + // Application.Popover = view; + + // // Assert + // Assert.Equal (view, Application.Popover); + + // Application.ResetState (ignoreDisposed: true); + //} + + //[Fact] + //public void Popover_SetToNull_HidesPreviousPopover () + //{ + // // Arrange + // var view = new View { Visible = true }; + // Application.Popover = view; + + // // Act + // Application.Popover = null; + + // // Assert + // Assert.False (view.Visible); + // Assert.Null (Application.Popover); + + // Application.ResetState (ignoreDisposed: true); + //} + + //[Fact] + //public void Popover_SetNewPopover_HidesPreviousPopover () + //{ + // // Arrange + // var oldView = new View { Visible = true }; + // var newView = new View (); + // Application.Popover = oldView; + + // // Act + // Application.Popover = newView; + + // // Assert + // Assert.False (oldView.Visible); + // Assert.Equal (newView, Application.Popover); + + // Application.ResetState (ignoreDisposed: true); + //} + + //[Fact] + //public void Popover_SetNewPopover_InitializesAndSetsProperties () + //{ + // // Arrange + // var view = new View (); + + // // Act + // Application.Popover = view; + + // // Assert + // Assert.True (view.IsInitialized); + // Assert.True (view.Arrangement.HasFlag (ViewArrangement.Overlapped)); + // Assert.Equal (Application.Top?.ColorScheme, view.ColorScheme); + + // Application.ResetState (ignoreDisposed: true); + //} +} diff --git a/Tests/UnitTests/Application/ApplicationTests.cs b/Tests/UnitTests/Application/ApplicationTests.cs index c98bbe3947..78798162d9 100644 --- a/Tests/UnitTests/Application/ApplicationTests.cs +++ b/Tests/UnitTests/Application/ApplicationTests.cs @@ -331,7 +331,8 @@ void CheckReset () Assert.Empty (Application._cachedViewsUnderMouse); // Mouse - Assert.Null (Application._lastMousePosition); + // Do not reset _lastMousePosition + //Assert.Null (Application._lastMousePosition); // Navigation Assert.Null (Application.Navigation); diff --git a/Tests/UnitTests/Configuration/ConfigurationMangerTests.cs b/Tests/UnitTests/Configuration/ConfigurationMangerTests.cs index 96239c1f6a..949136de2b 100644 --- a/Tests/UnitTests/Configuration/ConfigurationMangerTests.cs +++ b/Tests/UnitTests/Configuration/ConfigurationMangerTests.cs @@ -235,7 +235,7 @@ public void Load_Performance_Check () public void Load_Loads_Custom_Json () { // arrange - Locations = ConfigLocations.All; + Locations = ConfigLocations.Runtime | ConfigLocations.Default; Reset (); ThrowOnJsonErrors = true; diff --git a/Tests/UnitTests/Resources/ResourceManagerTests.cs b/Tests/UnitTests/Resources/ResourceManagerTests.cs index 15ebb0bdda..a77848da54 100644 --- a/Tests/UnitTests/Resources/ResourceManagerTests.cs +++ b/Tests/UnitTests/Resources/ResourceManagerTests.cs @@ -63,7 +63,10 @@ public void GetResourceSet_Without_Filter_Does_Not_Overflows_If_Key_Does_Not_Exi } [Fact] - public void GetString_Does_Not_Overflows_If_Key_Does_Not_Exist () { Assert.Null (GlobalResources.GetString (NO_EXISTENT_KEY, CultureInfo.CurrentCulture)); } + public void GetString_Does_Not_Overflows_If_Key_Does_Not_Exist () + { + Assert.Null (GlobalResources.GetString (NO_EXISTENT_KEY, CultureInfo.CurrentCulture)); + } [Fact] public void GetString_FallBack_To_Default_For_No_Existent_Culture_File () diff --git a/Tests/UnitTests/View/Mouse/GetViewsUnderMouseTests.cs b/Tests/UnitTests/View/Mouse/GetViewsUnderMouseTests.cs index 5e900ca5d7..ac37e62aa3 100644 --- a/Tests/UnitTests/View/Mouse/GetViewsUnderMouseTests.cs +++ b/Tests/UnitTests/View/Mouse/GetViewsUnderMouseTests.cs @@ -661,4 +661,57 @@ public void GetViewsUnderMouse_Tiled_SubViews (int mouseX, int mouseY, string [] Application.Top.Dispose (); Application.ResetState (true); } + + [Theory] + [InlineData (0, 0, new [] { "top" })] + [InlineData (9, 9, new [] { "top" })] + [InlineData (10, 10, new string [] { })] + [InlineData (-1, -1, new string [] { })] + [InlineData (1, 1, new [] { "top", "view" })] + [InlineData (1, 2, new [] { "top", "view" })] + [InlineData (2, 1, new [] { "top", "view" })] + [InlineData (2, 2, new [] { "top", "view", "popover" })] + [InlineData (3, 3, new [] { "top" })] // clipped + [InlineData (2, 3, new [] { "top" })] // clipped + public void GetViewsUnderMouse_Popover (int mouseX, int mouseY, string [] viewIdStrings) + { + // Arrange + Application.Top = new () + { + Frame = new (0, 0, 10, 10), + Id = "top" + }; + + var view = new View + { + Id = "view", + X = 1, + Y = 1, + Width = 2, + Height = 2, + Arrangement = ViewArrangement.Overlapped + }; // at 1,1 to 3,2 (screen) + + var popOver = new View + { + Id = "popover", + X = 1, + Y = 1, + Width = 2, + Height = 2, + Arrangement = ViewArrangement.Overlapped + }; // at 2,2 to 4,3 (screen) + + view.Add (popOver); + Application.Top.Add (view); + + List found = View.GetViewsUnderMouse (new (mouseX, mouseY)); + + string [] foundIds = found.Select (v => v!.Id).ToArray (); + + Assert.Equal (viewIdStrings, foundIds); + + Application.Top.Dispose (); + Application.ResetState (true); + } } diff --git a/Tests/UnitTests/Views/ContextMenuTests.cs b/Tests/UnitTests/Views/ContextMenuTests.cs index 4fc19d229d..b6a69063e3 100644 --- a/Tests/UnitTests/Views/ContextMenuTests.cs +++ b/Tests/UnitTests/Views/ContextMenuTests.cs @@ -5,7 +5,7 @@ namespace Terminal.Gui.ViewsTests; public class ContextMenuTests (ITestOutputHelper output) { - [Fact] + [Fact (Skip = "Redo for CMv2")] [AutoInitShutdown] public void ContextMenu_Constructors () { @@ -60,7 +60,7 @@ public void ContextMenu_Constructors () top.Dispose (); } - [Fact] + [Fact (Skip = "Redo for CMv2")] [AutoInitShutdown] public void ContextMenu_Is_Closed_If_Another_MenuBar_Is_Open_Or_Vice_Versa () { @@ -316,7 +316,7 @@ public void Draw_A_ContextMenu_Over_A_Top_Dialog () dialog.Dispose (); } - [Fact] + [Fact (Skip = "Redo for CMv2")] [AutoInitShutdown] public void ForceMinimumPosToZero_True_False () { @@ -366,7 +366,7 @@ public void ForceMinimumPosToZero_True_False () top.Dispose (); } - [Fact] + [Fact (Skip = "Redo for CMv2")] [AutoInitShutdown] public void Hide_Is_Invoke_At_Container_Closing () { @@ -395,25 +395,25 @@ public void Hide_Is_Invoke_At_Container_Closing () top.Dispose (); } - [Fact] - [AutoInitShutdown] - public void Key_Open_And_Close_The_ContextMenu () - { - var tf = new TextField (); - var top = new Toplevel (); - top.Add (tf); - Application.Begin (top); - - Assert.True (Application.RaiseKeyDownEvent (ContextMenu.DefaultKey)); - Assert.True (tf.ContextMenu.MenuBar!.IsMenuOpen); - Assert.True (Application.RaiseKeyDownEvent (ContextMenu.DefaultKey)); - - // The last context menu bar opened is always preserved - Assert.NotNull (tf.ContextMenu.MenuBar); - top.Dispose (); - } - - [Fact] + //[Fact (Skip = "Redo for CMv2")] + //[AutoInitShutdown] + //public void Key_Open_And_Close_The_ContextMenu () + //{ + // var tf = new TextField (); + // var top = new Toplevel (); + // top.Add (tf); + // Application.Begin (top); + + // Assert.True (Application.RaiseKeyDownEvent (ContextMenu.DefaultKey)); + // Assert.True (tf.ContextMenu.MenuBar!.IsMenuOpen); + // Assert.True (Application.RaiseKeyDownEvent (ContextMenu.DefaultKey)); + + // // The last context menu bar opened is always preserved + // Assert.False (tf.ContextMenu.Visible); + // top.Dispose (); + //} + + [Fact (Skip = "Redo for CMv2")] [AutoInitShutdown] public void KeyChanged_Event () { @@ -427,7 +427,7 @@ public void KeyChanged_Event () Assert.Equal (ContextMenu.DefaultKey, oldKey); } - [Fact] + [Fact (Skip = "Redo for CMv2")] [AutoInitShutdown] public void MenuItens_Changing () { @@ -479,7 +479,7 @@ public void MenuItens_Changing () top.Dispose (); } - [Fact] + [Fact (Skip = "Redo for CMv2")] [AutoInitShutdown] public void Menus_And_SubMenus_Always_Try_To_Be_On_Screen () { @@ -747,7 +747,7 @@ public void Menus_And_SubMenus_Always_Try_To_Be_On_Screen () top.Dispose (); } - [Fact] + [Fact (Skip = "Redo for CMv2")] [AutoInitShutdown] public void MouseFlags_Changing () { @@ -778,7 +778,7 @@ public void MouseFlags_Changing () top.Dispose (); } - [Fact] + [Fact (Skip = "Redo for CMv2")] public void MouseFlagsChanged_Event () { var oldMouseFlags = new MouseFlags (); @@ -791,7 +791,7 @@ public void MouseFlagsChanged_Event () Assert.Equal (MouseFlags.Button3Clicked, oldMouseFlags); } - [Fact] + [Fact (Skip = "Redo for CMv2")] [AutoInitShutdown] public void Position_Changing () { @@ -836,7 +836,7 @@ public void Position_Changing () top.Dispose (); } - [Fact] + [Fact (Skip = "Redo for CMv2")] [AutoInitShutdown] public void RequestStop_While_ContextMenu_Is_Open_Does_Not_Throws () { @@ -921,7 +921,7 @@ public void RequestStop_While_ContextMenu_Is_Open_Does_Not_Throws () top.Dispose (); } - [Fact] + [Fact (Skip = "Redo for CMv2")] [AutoInitShutdown] public void Show_Display_At_Zero_If_The_Toplevel_Height_Is_Less_Than_The_Menu_Height () { @@ -959,7 +959,7 @@ public void Show_Display_At_Zero_If_The_Toplevel_Height_Is_Less_Than_The_Menu_He top.Dispose (); } - [Fact] + [Fact (Skip = "Redo for CMv2")] [AutoInitShutdown] public void Show_Display_At_Zero_If_The_Toplevel_Width_Is_Less_Than_The_Menu_Width () { @@ -998,7 +998,7 @@ public void Show_Display_At_Zero_If_The_Toplevel_Width_Is_Less_Than_The_Menu_Wid top.Dispose (); } - [Fact] + [Fact (Skip = "Redo for CMv2")] [AutoInitShutdown] public void Show_Display_Below_The_Bottom_Host_If_Has_Enough_Space () { @@ -1073,7 +1073,7 @@ public void Show_Display_Below_The_Bottom_Host_If_Has_Enough_Space () top.Dispose (); } - [Fact] + [Fact (Skip = "Redo for CMv2")] [AutoInitShutdown] public void Show_Ensures_Display_Inside_The_Container_But_Preserves_Position () { @@ -1111,7 +1111,7 @@ public void Show_Ensures_Display_Inside_The_Container_But_Preserves_Position () top.Dispose (); } - [Fact] + [Fact (Skip = "Redo for CMv2")] [AutoInitShutdown] public void Show_Ensures_Display_Inside_The_Container_Without_Overlap_The_Host () { @@ -1162,7 +1162,7 @@ public void Show_Ensures_Display_Inside_The_Container_Without_Overlap_The_Host ( top.Dispose (); } - [Fact] + [Fact (Skip = "Redo for CMv2")] [AutoInitShutdown] public void Show_Hide_IsShow () { @@ -1201,7 +1201,7 @@ public void Show_Hide_IsShow () top.Dispose (); } - [Fact] + [Fact (Skip = "Redo for CMv2")] [AutoInitShutdown] public void UseSubMenusSingleFrame_True_By_Mouse () { @@ -1288,7 +1288,7 @@ public void UseSubMenusSingleFrame_True_By_Mouse () top.Dispose (); } - [Fact] + [Fact (Skip = "Redo for CMv2")] [AutoInitShutdown] public void UseSubMenusSingleFrame_False_By_Mouse () { @@ -1404,7 +1404,7 @@ public void UseSubMenusSingleFrame_False_By_Mouse () top.Dispose (); } - [Fact] + [Fact (Skip = "Redo for CMv2")] [AutoInitShutdown] public void Handling_TextField_With_Opened_ContextMenu_By_Mouse_HasFocus () { @@ -1424,7 +1424,7 @@ public void Handling_TextField_With_Opened_ContextMenu_By_Mouse_HasFocus () Assert.False (tf1.HasFocus); Assert.False (tf2.HasFocus); Assert.Equal (6, win.SubViews.Count); - Assert.True (tf2.ContextMenu.MenuBar.IsMenuOpen); + //Assert.True (tf2.ContextMenu.IsMenuOpen); Assert.True (win.Focused is Menu); Assert.True (Application.MouseGrabView is Menu); Assert.Equal (tf2, Application._cachedViewsUnderMouse.LastOrDefault ()); @@ -1436,7 +1436,7 @@ public void Handling_TextField_With_Opened_ContextMenu_By_Mouse_HasFocus () Assert.Equal (5, win.SubViews.Count); // The last context menu bar opened is always preserved - Assert.NotNull (tf2.ContextMenu.MenuBar); + Assert.NotNull (tf2.ContextMenu); Assert.Equal (win.Focused, tf1); Assert.Null (Application.MouseGrabView); Assert.Equal (tf1, Application._cachedViewsUnderMouse.LastOrDefault ()); @@ -1448,7 +1448,7 @@ public void Handling_TextField_With_Opened_ContextMenu_By_Mouse_HasFocus () Assert.Equal (5, win.SubViews.Count); // The last context menu bar opened is always preserved - Assert.NotNull (tf2.ContextMenu.MenuBar); + Assert.NotNull (tf2.ContextMenu); Assert.Equal (win.Focused, tf2); Assert.Null (Application.MouseGrabView); Assert.Equal (tf2, Application._cachedViewsUnderMouse.LastOrDefault ()); @@ -1457,7 +1457,7 @@ public void Handling_TextField_With_Opened_ContextMenu_By_Mouse_HasFocus () win.Dispose (); } - [Fact] + [Fact (Skip = "Redo for CMv2")] [AutoInitShutdown] public void Empty_Menus_Items_Children_Does_Not_Open_The_Menu () { @@ -1473,7 +1473,7 @@ public void Empty_Menus_Items_Children_Does_Not_Open_The_Menu () top.Dispose (); } - [Fact] + [Fact (Skip = "Redo for CMv2")] [AutoInitShutdown] public void KeyBindings_Removed_On_Close_ContextMenu () { @@ -1544,7 +1544,7 @@ public void KeyBindings_Removed_On_Close_ContextMenu () void Delete () { deleteFile = true; } } - [Fact] + [Fact (Skip = "Redo for CMv2")] [AutoInitShutdown] public void KeyBindings_With_ContextMenu_And_MenuBar () { @@ -1623,7 +1623,7 @@ public void KeyBindings_With_ContextMenu_And_MenuBar () void Rename () { renameFile = true; } } - [Fact] + [Fact (Skip = "Redo for CMv2")] [AutoInitShutdown] public void KeyBindings_With_Same_Shortcut_ContextMenu_And_MenuBar () { @@ -1693,7 +1693,7 @@ public void KeyBindings_With_Same_Shortcut_ContextMenu_And_MenuBar () void NewContextMenu () { newContextMenu = true; } } - [Fact] + [Fact (Skip = "Redo for CMv2")] [AutoInitShutdown] public void HotKeys_Removed_On_Close_ContextMenu () { @@ -1779,7 +1779,7 @@ public void HotKeys_Removed_On_Close_ContextMenu () void Delete () { deleteFile = true; } } - [Fact] + [Fact (Skip = "Redo for CMv2")] [AutoInitShutdown] public void HotKeys_With_ContextMenu_And_MenuBar () { @@ -1911,7 +1911,7 @@ public void HotKeys_With_ContextMenu_And_MenuBar () void Rename () { renameFile = true; } } - [Fact] + [Fact (Skip = "Redo for CMv2")] [AutoInitShutdown] public void Opened_MenuBar_Is_Closed_When_Another_MenuBar_Is_Opening_Also_By_HotKey () { diff --git a/Tests/UnitTests/Views/TextFieldTests.cs b/Tests/UnitTests/Views/TextFieldTests.cs index 14c84a282f..76e970706f 100644 --- a/Tests/UnitTests/Views/TextFieldTests.cs +++ b/Tests/UnitTests/Views/TextFieldTests.cs @@ -195,7 +195,7 @@ public void CaptionedTextField_DoesNotOverspillViewport_Unicode () Application.Top.Dispose (); } - [Theory] + [Theory (Skip = "Broke with ContextMenuv2")] [AutoInitShutdown] [InlineData ("blah")] [InlineData (" ")] diff --git a/Tests/UnitTests/Views/TextViewTests.cs b/Tests/UnitTests/Views/TextViewTests.cs index 6a71e63b12..02de26bfb4 100644 --- a/Tests/UnitTests/Views/TextViewTests.cs +++ b/Tests/UnitTests/Views/TextViewTests.cs @@ -5534,7 +5534,7 @@ public void KeyBindings_Command () Assert.False (tv.NewKeyDownEvent (Application.PrevTabGroupKey)); Assert.True (tv.NewKeyDownEvent (ContextMenu.DefaultKey)); - Assert.True (tv.ContextMenu != null && tv.ContextMenu.MenuBar.Visible); + Assert.True (tv.ContextMenu != null && tv.ContextMenu.Visible); top.Dispose (); } diff --git a/Tests/UnitTestsParallelizable/Application/ApplicationPopoverTests.cs b/Tests/UnitTestsParallelizable/Application/ApplicationPopoverTests.cs new file mode 100644 index 0000000000..060c26f580 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Application/ApplicationPopoverTests.cs @@ -0,0 +1,163 @@ +using Moq; + +namespace Terminal.Gui.ApplicationTests; + +public class ApplicationPopoverTests +{ + [Fact] + public void Register_AddsPopover () + { + // Arrange + var popover = new Mock ().Object; + var popoverManager = new ApplicationPopover (); + + // Act + popoverManager.Register (popover); + + // Assert + Assert.Contains (popover, popoverManager.Popovers); + } + + [Fact] + public void DeRegister_RemovesPopover () + { + // Arrange + var popover = new Mock ().Object; + var popoverManager = new ApplicationPopover (); + popoverManager.Register (popover); + + // Act + var result = popoverManager.DeRegister (popover); + + // Assert + Assert.True (result); + Assert.DoesNotContain (popover, popoverManager.Popovers); + } + + [Fact] + public void ShowPopover_SetsActivePopover () + { + // Arrange + var popover = new Mock ().Object; + var popoverManager = new ApplicationPopover (); + + // Act + popoverManager.ShowPopover (popover); + + // Assert + Assert.Equal (popover, popoverManager.GetActivePopover ()); + } + + [Fact] + public void HidePopover_ClearsActivePopover () + { + // Arrange + var popover = new Mock ().Object; + var popoverManager = new ApplicationPopover (); + popoverManager.ShowPopover (popover); + + // Act + popoverManager.HidePopover (popover); + + // Assert + Assert.Null (popoverManager.GetActivePopover ()); + } + + + [Fact] + public void DispatchKeyDown_ActivePopoverGetsKey () + { + // Arrange + var popover = new IPopoverTestClass (); + var popoverManager = new ApplicationPopover (); + popoverManager.ShowPopover (popover); + + // Act + popoverManager.DispatchKeyDown (Key.A); + + // Assert + Assert.Contains (KeyCode.A, popover.HandledKeys); + } + + + [Fact] + public void DispatchKeyDown_ActivePopoverGetsHotKey () + { + // Arrange + var popover = new IPopoverTestClass (); + var popoverManager = new ApplicationPopover (); + popoverManager.ShowPopover (popover); + + // Act + popoverManager.DispatchKeyDown (Key.N.WithCtrl); + + // Assert + Assert.Equal(1, popover.NewCommandInvokeCount); + Assert.Contains (Key.N.WithCtrl, popover.HandledKeys); + } + + + [Fact] + public void DispatchKeyDown_InactivePopoverGetsHotKey () + { + // Arrange + var activePopover = new IPopoverTestClass () { Id = "activePopover" }; + var inactivePopover = new IPopoverTestClass () { Id = "inactivePopover" }; ; + var popoverManager = new ApplicationPopover (); + popoverManager.ShowPopover (activePopover); + popoverManager.Register (inactivePopover); + + // Act + popoverManager.DispatchKeyDown (Key.N.WithCtrl); + + // Assert + Assert.Equal (1, activePopover.NewCommandInvokeCount); + Assert.Equal (1, inactivePopover.NewCommandInvokeCount); + Assert.Contains (Key.N.WithCtrl, activePopover.HandledKeys); + Assert.NotEmpty (inactivePopover.HandledKeys); + } + + [Fact] + public void DispatchKeyDown_InactivePopoverDoesGetKey () + { + // Arrange + var activePopover = new IPopoverTestClass (); + var inactivePopover = new IPopoverTestClass (); + var popoverManager = new ApplicationPopover (); + popoverManager.ShowPopover (activePopover); + popoverManager.Register (inactivePopover); + + // Act + popoverManager.DispatchKeyDown (Key.A); + + // Assert + Assert.Contains (Key.A, activePopover.HandledKeys); + Assert.NotEmpty (inactivePopover.HandledKeys); + } + + public class IPopoverTestClass : View, IPopover + { + public List HandledKeys { get; } = new List (); + public int NewCommandInvokeCount { get; private set; } + + public IPopoverTestClass () + { + CanFocus = true; + AddCommand(Command.New, NewCommandHandler ); + HotKeyBindings.Add (Key.N.WithCtrl, Command.New); + + bool? NewCommandHandler (ICommandContext ctx) + { + NewCommandInvokeCount++; + + return false; + } + } + + protected override bool OnKeyDown (Key key) + { + HandledKeys.Add (key); + return false; + } + } +} diff --git a/Tests/UnitTestsParallelizable/Drawing/DrawContextTests.cs b/Tests/UnitTestsParallelizable/Drawing/DrawContextTests.cs new file mode 100644 index 0000000000..9d30360b8c --- /dev/null +++ b/Tests/UnitTestsParallelizable/Drawing/DrawContextTests.cs @@ -0,0 +1,20 @@ +namespace Terminal.Gui.DrawingTests; + +public class DrawContextTests +{ + [Fact (Skip = "Region Union is broken")] + public void AddDrawnRectangle_Unions () + { + DrawContext drawContext = new DrawContext (); + + drawContext.AddDrawnRectangle (new (0, 0, 1, 1)); + drawContext.AddDrawnRectangle (new (1, 0, 1, 1)); + + Assert.Equal (new Rectangle (0, 0, 2, 1), drawContext.GetDrawnRegion ().GetBounds ()); + Assert.Equal (2, drawContext.GetDrawnRegion ().GetRectangles ().Length); + + drawContext.AddDrawnRectangle (new (0, 0, 4, 1)); + Assert.Equal (new Rectangle (0, 1, 4, 1), drawContext.GetDrawnRegion ().GetBounds ()); + Assert.Single (drawContext.GetDrawnRegion ().GetRectangles ()); + } +} \ No newline at end of file diff --git a/Tests/UnitTestsParallelizable/Drawing/Region/RegionTests.cs b/Tests/UnitTestsParallelizable/Drawing/Region/RegionTests.cs index 2350379c85..cd8f3895ba 100644 --- a/Tests/UnitTestsParallelizable/Drawing/Region/RegionTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/Region/RegionTests.cs @@ -783,6 +783,46 @@ public void Union_Region_MergesRegions () Assert.True (region1.Contains (40, 40)); } + [Fact (Skip = "Union is broken")] + public void Union_Third_Rect_Covering_Two_Disjoint_Merges () + { + var origRegion = new Region (); + + var region1 = new Region (new (0, 0, 1, 1)); + var region2 = new Region (new (1, 0, 1, 1)); + + origRegion.Union(region1); + origRegion.Union(region2); + + Assert.Equal (new Rectangle (0, 0, 2, 1), origRegion.GetBounds ()); + Assert.Equal (2, origRegion.GetRectangles ().Length); + + origRegion.Union(new Region(new (0, 0, 4, 1))); + + Assert.Equal (new Rectangle (0, 1, 4, 1), origRegion.GetBounds ()); + Assert.Single (origRegion.GetRectangles ()); + } + + [Fact (Skip = "MinimalUnion is broken")] + public void MinimalUnion_Third_Rect_Covering_Two_Disjoint_Merges () + { + var origRegion = new Region (); + + var region1 = new Region (new (0, 0, 1, 1)); + var region2 = new Region (new (1, 0, 1, 1)); + + origRegion.Union (region1); + origRegion.Union (region2); + + Assert.Equal (new Rectangle (0, 0, 2, 1), origRegion.GetBounds ()); + Assert.Equal (2, origRegion.GetRectangles ().Length); + + origRegion.MinimalUnion (new Region (new (0, 0, 4, 1))); + + Assert.Equal (new Rectangle (0, 1, 4, 1), origRegion.GetBounds ()); + Assert.Single (origRegion.GetRectangles ()); + } + /// /// Proves MergeRegion does not overly combine regions. /// diff --git a/Tests/UnitTestsParallelizable/View/ViewCommandTests.cs b/Tests/UnitTestsParallelizable/View/ViewCommandTests.cs index 96434d9905..5671dbd9e3 100644 --- a/Tests/UnitTestsParallelizable/View/ViewCommandTests.cs +++ b/Tests/UnitTestsParallelizable/View/ViewCommandTests.cs @@ -226,10 +226,52 @@ public void HotKey_Command_SetsFocus () #endregion OnHotKey/HotKey tests + #region InvokeCommand Tests + + + [Fact] + public void InvokeCommand_NotBound_Invokes_CommandNotBound () + { + ViewEventTester view = new (); + + view.InvokeCommand (Command.NotBound); + + Assert.False (view.HasFocus); + Assert.Equal (1, view.OnCommandNotBoundCount); + Assert.Equal (1, view.CommandNotBoundCount); + } + + [Fact] + public void InvokeCommand_Command_Not_Bound_Invokes_CommandNotBound () + { + ViewEventTester view = new (); + + view.InvokeCommand (Command.New); + + Assert.False (view.HasFocus); + Assert.Equal (1, view.OnCommandNotBoundCount); + Assert.Equal (1, view.CommandNotBoundCount); + } + + [Fact] + public void InvokeCommand_Command_Bound_Does_Not_Invoke_CommandNotBound () + { + ViewEventTester view = new (); + + view.InvokeCommand (Command.Accept); + + Assert.False (view.HasFocus); + Assert.Equal (0, view.OnCommandNotBoundCount); + Assert.Equal (0, view.CommandNotBoundCount); + } + + #endregion + public class ViewEventTester : View { public ViewEventTester () { + Id = "viewEventTester"; CanFocus = true; Accepting += (s, a) => @@ -249,6 +291,12 @@ public ViewEventTester () a.Cancel = HandleSelecting; SelectingCount++; }; + + CommandNotBound += (s, a) => + { + a.Cancel = HandleCommandNotBound; + CommandNotBoundCount++; + }; } public int OnAcceptedCount { get; set; } @@ -282,6 +330,8 @@ protected override bool OnHandlingHotKey (CommandEventArgs args) public int OnSelectingCount { get; set; } public int SelectingCount { get; set; } public bool HandleOnSelecting { get; set; } + public bool HandleSelecting { get; set; } + /// protected override bool OnSelecting (CommandEventArgs args) @@ -291,6 +341,17 @@ protected override bool OnSelecting (CommandEventArgs args) return HandleOnSelecting; } - public bool HandleSelecting { get; set; } + public int OnCommandNotBoundCount { get; set; } + public int CommandNotBoundCount { get; set; } + + public bool HandleOnCommandNotBound { get; set; } + + public bool HandleCommandNotBound { get; set; } + + protected override bool OnCommandNotBound (CommandEventArgs args) + { + OnCommandNotBoundCount++; + return HandleOnCommandNotBound; + } } } diff --git a/UICatalog/Scenarios/Arrangement.cs b/UICatalog/Scenarios/Arrangement.cs index 403e6f5baf..c7eea3e2b5 100644 --- a/UICatalog/Scenarios/Arrangement.cs +++ b/UICatalog/Scenarios/Arrangement.cs @@ -198,6 +198,9 @@ public override void Main () testFrame.Add (movableSizeableWithProgress); testFrame.Add (transparentView); + + testFrame.Add (new TransparentView ()); + adornmentsEditor.AutoSelectSuperView = testFrame; arrangementEditor.AutoSelectSuperView = testFrame; @@ -312,6 +315,31 @@ public override List GetDemoKeyStrokes () return keys; } + + public class TransparentView : FrameView + { + public TransparentView() + { + Title = "Transparent"; + Text = "Text"; + X = 0; + Y = 0; + Width = 30; + Height = 10; + Arrangement = ViewArrangement.Overlapped | ViewArrangement.Resizable | ViewArrangement.Movable; + ViewportSettings |= Terminal.Gui.ViewportSettings.Transparent; + + Padding!.Thickness = new Thickness (1); + + Add ( + new Button () + { + Title = "_Hi", + X = Pos.Center (), + Y = Pos.Center () + }); + } + } } public class TransparentView : FrameView diff --git a/UICatalog/Scenarios/Bars.cs b/UICatalog/Scenarios/Bars.cs index f6e511b1f5..444c8e89f4 100644 --- a/UICatalog/Scenarios/Bars.cs +++ b/UICatalog/Scenarios/Bars.cs @@ -81,15 +81,15 @@ private void App_Loaded (object sender, EventArgs e) }; menuBarLikeExamples.Add (label); - bar = new MenuBarv2 - { - Id = "menuBar", - X = Pos.Right (label), - Y = Pos.Top (label), - }; - - ConfigMenuBar (bar); - menuBarLikeExamples.Add (bar); + //bar = new MenuBarv2 + //{ + // Id = "menuBar", + // X = Pos.Right (label), + // Y = Pos.Top (label), + //}; + + //ConfigMenuBar (bar); + //menuBarLikeExamples.Add (bar); FrameView menuLikeExamples = new () { diff --git a/UICatalog/Scenarios/ColorPicker.cs b/UICatalog/Scenarios/ColorPicker.cs index cd0789d606..83e5f7536f 100644 --- a/UICatalog/Scenarios/ColorPicker.cs +++ b/UICatalog/Scenarios/ColorPicker.cs @@ -250,7 +250,7 @@ private void ForegroundColor_ColorChanged (object sender, EventArgs e) /// Update a color label from his ColorPicker. private void UpdateColorLabel (Label label, Color color) { - label.ClearViewport (); + label.ClearViewport (null); label.Text = $"{color} ({(int)color}) #{color.R:X2}{color.G:X2}{color.B:X2}"; diff --git a/UICatalog/Scenarios/ContextMenus.cs b/UICatalog/Scenarios/ContextMenus.cs index f609f3562d..0d9d50e38b 100644 --- a/UICatalog/Scenarios/ContextMenus.cs +++ b/UICatalog/Scenarios/ContextMenus.cs @@ -1,6 +1,5 @@ -using System.Collections.Generic; -using System.Globalization; -using System.Threading; +using System.Globalization; +using JetBrains.Annotations; using Terminal.Gui; namespace UICatalog.Scenarios; @@ -9,38 +8,37 @@ namespace UICatalog.Scenarios; [ScenarioCategory ("Menus")] public class ContextMenus : Scenario { - private List _cultureInfos = null; - private ContextMenu _contextMenu = new (); - private bool _forceMinimumPosToZero = true; - private MenuItem _miForceMinimumPosToZero; - private MenuItem _miUseSubMenusSingleFrame; + [CanBeNull] + private ContextMenuv2 _winContextMenu; private TextField _tfTopLeft, _tfTopRight, _tfMiddle, _tfBottomLeft, _tfBottomRight; - private bool _useSubMenusSingleFrame; + private readonly List _cultureInfos = Application.SupportedCultures; + private readonly Key _winContextMenuKey = Key.Space.WithCtrl; public override void Main () { // Init Application.Init (); - _cultureInfos = Application.SupportedCultures; // Setup - Create a top-level application window and configure it. Window appWindow = new () { Title = GetQuitKeyAndName (), - Arrangement = ViewArrangement.Fixed + Arrangement = ViewArrangement.Fixed, + ColorScheme = Colors.ColorSchemes ["Toplevel"] }; var text = "Context Menu"; var width = 20; - var winContextMenuKey = (KeyCode)Key.Space.WithCtrl; + + CreateWinContextMenu (); var label = new Label { - X = Pos.Center (), Y = 1, Text = $"Press '{winContextMenuKey}' to open the Window context menu." + X = Pos.Center (), Y = 1, Text = $"Press '{_winContextMenuKey}' to open the Window context menu." }; appWindow.Add (label); - label = new() + label = new () { X = Pos.Center (), Y = Pos.Bottom (label), @@ -48,252 +46,198 @@ public override void Main () }; appWindow.Add (label); - _tfTopLeft = new() { Width = width, Text = text }; + _tfTopLeft = new () { Id = "_tfTopLeft", Width = width, Text = text }; appWindow.Add (_tfTopLeft); - _tfTopRight = new() { X = Pos.AnchorEnd (width), Width = width, Text = text }; + _tfTopRight = new () { Id = "_tfTopRight", X = Pos.AnchorEnd (width), Width = width, Text = text }; appWindow.Add (_tfTopRight); - _tfMiddle = new() { X = Pos.Center (), Y = Pos.Center (), Width = width, Text = text }; + _tfMiddle = new () { Id = "_tfMiddle", X = Pos.Center (), Y = Pos.Center (), Width = width, Text = text }; appWindow.Add (_tfMiddle); - _tfBottomLeft = new() { Y = Pos.AnchorEnd (1), Width = width, Text = text }; + _tfBottomLeft = new () { Id = "_tfBottomLeft", Y = Pos.AnchorEnd (1), Width = width, Text = text }; appWindow.Add (_tfBottomLeft); - _tfBottomRight = new() { X = Pos.AnchorEnd (width), Y = Pos.AnchorEnd (1), Width = width, Text = text }; + _tfBottomRight = new () { Id = "_tfBottomRight", X = Pos.AnchorEnd (width), Y = Pos.AnchorEnd (1), Width = width, Text = text }; appWindow.Add (_tfBottomRight); - Point mousePos = default; - - appWindow.KeyDown += (s, e) => - { - if (e.KeyCode == winContextMenuKey) - { - ShowContextMenu (mousePos.X, mousePos.Y); - e.Handled = true; - } - }; + appWindow.KeyDown += OnAppWindowOnKeyDown; + appWindow.MouseClick += OnAppWindowOnMouseClick; - appWindow.MouseClick += (s, e) => - { - if (e.Flags == _contextMenu.MouseFlags) - { - ShowContextMenu (e.Position.X, e.Position.Y); - e.Handled = true; - } - }; + CultureInfo originalCulture = Thread.CurrentThread.CurrentUICulture; + appWindow.Closed += (s, e) => { Thread.CurrentThread.CurrentUICulture = originalCulture; }; - Application.MouseEvent += ApplicationMouseEvent; + // Run - Start the application. + Application.Run (appWindow); + appWindow.Dispose (); + appWindow.KeyDown -= OnAppWindowOnKeyDown; + appWindow.MouseClick -= OnAppWindowOnMouseClick; + _winContextMenu?.Dispose (); - void ApplicationMouseEvent (object sender, MouseEventArgs a) { mousePos = a.Position; } + // Shutdown - Calling Application.Shutdown is required. + Application.Shutdown (); - appWindow.WantMousePositionReports = true; + return; - appWindow.Closed += (s, e) => - { - Thread.CurrentThread.CurrentUICulture = new ("en-US"); - Application.MouseEvent -= ApplicationMouseEvent; - }; + void OnAppWindowOnMouseClick (object s, MouseEventArgs e) + { + if (e.Flags == MouseFlags.Button3Clicked) + { + // ReSharper disable once AccessToDisposedClosure + _winContextMenu?.MakeVisible (e.ScreenPosition); + e.Handled = true; + } + } - var top = new Toplevel (); - top.Add (appWindow); + void OnAppWindowOnKeyDown (object s, Key e) + { + if (e == _winContextMenuKey) + { + // ReSharper disable once AccessToDisposedClosure + _winContextMenu?.MakeVisible (); + e.Handled = true; + } + } + } - // Run - Start the application. - Application.Run (top); - top.Dispose (); + private void CreateWinContextMenu () + { + if (_winContextMenu is { }) + { + _winContextMenu.Dispose (); + _winContextMenu = null; + } - // Shutdown - Calling Application.Shutdown is required. - Application.Shutdown (); + _winContextMenu = new ( + [ + new MenuItemv2 + { + Title = "C_ultures", + SubMenu = GetSupportedCultureMenu (), + }, + new Line (), + new MenuItemv2 + { + Title = "_Configuration...", + HelpText = "Show configuration", + Action = () => MessageBox.Query ( + 50, + 10, + "Configuration", + "This would be a configuration dialog", + "Ok" + ) + }, + new MenuItemv2 + { + Title = "M_ore options", + SubMenu = new ( + [ + new MenuItemv2 + { + Title = "_Setup...", + HelpText = "Perform setup", + Action = () => MessageBox + .Query ( + 50, + 10, + "Setup", + "This would be a setup dialog", + "Ok" + ), + Key = Key.T.WithCtrl + }, + new MenuItemv2 + { + Title = "_Maintenance...", + HelpText = "Maintenance mode", + Action = () => MessageBox + .Query ( + 50, + 10, + "Maintenance", + "This would be a maintenance dialog", + "Ok" + ) + } + ]) + }, + new Line (), + new MenuItemv2 + { + Title = "_Quit", + Action = () => Application.RequestStop () + } + ]) + { + Key = _winContextMenuKey + }; } - private MenuItem [] GetSupportedCultures () + private Menuv2 GetSupportedCultureMenu () { - List supportedCultures = new (); + List supportedCultures = []; int index = -1; - if (_cultureInfos == null) - { - return supportedCultures.ToArray (); - } - foreach (CultureInfo c in _cultureInfos) { - var culture = new MenuItem { CheckType = MenuItemCheckStyle.Checked }; + MenuItemv2 culture = new (); + + culture.CommandView = new CheckBox { CanFocus = false, HighlightStyle = HighlightStyle.None }; if (index == -1) { + // Create English because GetSupportedCutures doesn't include it + culture.Id = "_English"; culture.Title = "_English"; - culture.Help = "en-US"; - culture.Checked = Thread.CurrentThread.CurrentUICulture.Name == "en-US"; + culture.HelpText = "en-US"; + + ((CheckBox)culture.CommandView).CheckedState = + Thread.CurrentThread.CurrentUICulture.Name == "en-US" ? CheckState.Checked : CheckState.UnChecked; CreateAction (supportedCultures, culture); supportedCultures.Add (culture); + index++; - culture = new() { CheckType = MenuItemCheckStyle.Checked }; + culture = new (); + culture.CommandView = new CheckBox { CanFocus = false, HighlightStyle = HighlightStyle.None }; } + culture.Id = $"_{c.Parent.EnglishName}"; culture.Title = $"_{c.Parent.EnglishName}"; - culture.Help = c.Name; - culture.Checked = Thread.CurrentThread.CurrentUICulture.Name == c.Name; + culture.HelpText = c.Name; + + ((CheckBox)culture.CommandView).CheckedState = + Thread.CurrentThread.CurrentUICulture.Name == culture.HelpText ? CheckState.Checked : CheckState.UnChecked; CreateAction (supportedCultures, culture); supportedCultures.Add (culture); } - return supportedCultures.ToArray (); + Menuv2 menu = new (supportedCultures.ToArray ()); + menu.Border.LineStyle = LineStyle.None; + menu.Border.Thickness = new (0,0,0,0); + + // menu.Padding.Thickness = new (1); - void CreateAction (List supportedCultures, MenuItem culture) + return menu; + + void CreateAction (List cultures, MenuItemv2 culture) { culture.Action += () => { - Thread.CurrentThread.CurrentUICulture = new (culture.Help); - culture.Checked = true; + Thread.CurrentThread.CurrentUICulture = new (culture.HelpText); - foreach (MenuItem item in supportedCultures) + foreach (MenuItemv2 item in cultures) { - item.Checked = item.Help == Thread.CurrentThread.CurrentUICulture.Name; + ((CheckBox)item.CommandView).CheckedState = + Thread.CurrentThread.CurrentUICulture.Name == item.HelpText ? CheckState.Checked : CheckState.UnChecked; } }; } } - private void ShowContextMenu (int x, int y) - { - _contextMenu = new() - { - Position = new (x, y), - ForceMinimumPosToZero = _forceMinimumPosToZero, - UseSubMenusSingleFrame = _useSubMenusSingleFrame - }; - - MenuBarItem menuItems = new ( - new [] - { - new MenuBarItem ( - "_Languages", - GetSupportedCultures () - ), - new ( - "_Configuration", - "Show configuration", - () => MessageBox.Query ( - 50, - 5, - "Info", - "This would open settings dialog", - "Ok" - ) - ), - new MenuBarItem ( - "M_ore options", - new MenuItem [] - { - new ( - "_Setup", - "Change settings", - () => MessageBox - .Query ( - 50, - 5, - "Info", - "This would open setup dialog", - "Ok" - ), - shortcutKey: KeyCode.T - | KeyCode - .CtrlMask - ), - new ( - "_Maintenance", - "Maintenance mode", - () => MessageBox - .Query ( - 50, - 5, - "Info", - "This would open maintenance dialog", - "Ok" - ) - ) - } - ), - _miForceMinimumPosToZero = - new ( - "Fo_rceMinimumPosToZero", - "", - () => - { - _miForceMinimumPosToZero - .Checked = - _forceMinimumPosToZero = - !_forceMinimumPosToZero; - - _tfTopLeft.ContextMenu - .ForceMinimumPosToZero = - _forceMinimumPosToZero; - - _tfTopRight.ContextMenu - .ForceMinimumPosToZero = - _forceMinimumPosToZero; - - _tfMiddle.ContextMenu - .ForceMinimumPosToZero = - _forceMinimumPosToZero; - - _tfBottomLeft.ContextMenu - .ForceMinimumPosToZero = - _forceMinimumPosToZero; - - _tfBottomRight - .ContextMenu - .ForceMinimumPosToZero = - _forceMinimumPosToZero; - } - ) - { - CheckType = - MenuItemCheckStyle - .Checked, - Checked = - _forceMinimumPosToZero - }, - _miUseSubMenusSingleFrame = - new ( - "Use_SubMenusSingleFrame", - "", - () => _contextMenu - .UseSubMenusSingleFrame = - (bool) - (_miUseSubMenusSingleFrame - .Checked = - _useSubMenusSingleFrame = - !_useSubMenusSingleFrame) - ) - { - CheckType = MenuItemCheckStyle - .Checked, - Checked = - _useSubMenusSingleFrame - }, - null, - new ( - "_Quit", - "", - () => Application.RequestStop () - ) - } - ); - _tfTopLeft.ContextMenu.ForceMinimumPosToZero = _forceMinimumPosToZero; - _tfTopRight.ContextMenu.ForceMinimumPosToZero = _forceMinimumPosToZero; - _tfMiddle.ContextMenu.ForceMinimumPosToZero = _forceMinimumPosToZero; - _tfBottomLeft.ContextMenu.ForceMinimumPosToZero = _forceMinimumPosToZero; - _tfBottomRight.ContextMenu.ForceMinimumPosToZero = _forceMinimumPosToZero; - - _contextMenu.Show (menuItems); - } - - public override List GetDemoKeyStrokes () { - var keys = new List (); + List keys = new (); keys.Add (Key.F10.WithShift); keys.Add (Key.Esc); diff --git a/UICatalog/Scenarios/Editor.cs b/UICatalog/Scenarios/Editor.cs index 4bb184d8ab..5f1176bd14 100644 --- a/UICatalog/Scenarios/Editor.cs +++ b/UICatalog/Scenarios/Editor.cs @@ -225,12 +225,12 @@ public override void Main () "", () => { - _miForceMinimumPosToZero.Checked = - _forceMinimumPosToZero = - !_forceMinimumPosToZero; + //_miForceMinimumPosToZero.Checked = + // _forceMinimumPosToZero = + // !_forceMinimumPosToZero; - _textView.ContextMenu.ForceMinimumPosToZero = - _forceMinimumPosToZero; + //_textView.ContextMenu.ForceMinimumPosToZero = + // _forceMinimumPosToZero; } ) { diff --git a/UICatalog/Scenarios/Generic.cs b/UICatalog/Scenarios/Generic.cs index 33ca9e73a8..e9bfb2cfce 100644 --- a/UICatalog/Scenarios/Generic.cs +++ b/UICatalog/Scenarios/Generic.cs @@ -18,19 +18,11 @@ public override void Main () Title = GetQuitKeyAndName (), }; - FrameView frame = new () - { - Height = Dim.Fill (), - Width = Dim.Fill (), - Title = "Frame" - }; - appWindow.Add (frame); - var button = new Shortcut () { - Id = "button", - X = Pos.Center (), - Y = 1, + Id = "button", + X = Pos.Center (), + Y = 1, Text = "_Press me!" }; @@ -41,7 +33,7 @@ public override void Main () MessageBox.ErrorQuery ("Error", "You pressed the button!", "_Ok"); }; - frame.Add (button); + appWindow.Add (button); // Run - Start the application. Application.Run (appWindow); diff --git a/UICatalog/Scenarios/MenusV2.cs b/UICatalog/Scenarios/MenusV2.cs new file mode 100644 index 0000000000..756efd9dfc --- /dev/null +++ b/UICatalog/Scenarios/MenusV2.cs @@ -0,0 +1,556 @@ +#nullable enable + +using System.Collections.ObjectModel; +using Microsoft.Extensions.Logging; +using Serilog; +using Serilog.Core; +using Serilog.Events; +using Terminal.Gui; +using ILogger = Microsoft.Extensions.Logging.ILogger; + +namespace UICatalog.Scenarios; + +[ScenarioMetadata ("MenusV2", "Illustrates MenuV2")] +[ScenarioCategory ("Controls")] +[ScenarioCategory ("Shortcuts")] +public class MenusV2 : Scenario +{ + public override void Main () + { + Logging.Logger = CreateLogger (); + + Application.Init (); + Toplevel app = new (); + app.Title = GetQuitKeyAndName (); + + ObservableCollection eventSource = new (); + + var eventLog = new ListView + { + Title = "Event Log", + X = Pos.AnchorEnd (), + Width = Dim.Auto (), + Height = Dim.Fill (), // Make room for some wide things + ColorScheme = Colors.ColorSchemes ["Toplevel"], + Source = new ListWrapper (eventSource) + }; + eventLog.Border!.Thickness = new (0, 1, 0, 0); + + TargetView targetView = new () + { + Id = "targetView", + Title = "Target View", + + X = 5, + Y = 5, + Width = Dim.Fill (2)! - Dim.Width (eventLog), + Height = Dim.Fill (2), + BorderStyle = LineStyle.Dotted + }; + app.Add (targetView); + + targetView.CommandNotBound += (o, args) => + { + if (args.Cancel) + { + return; + } + + Logging.Trace ($"targetView CommandNotBound: {args?.Context?.Command}"); + eventSource.Add ($"targetView CommandNotBound: {args?.Context?.Command}"); + eventLog.MoveDown (); + }; + + targetView.Accepting += (o, args) => + { + if (args.Cancel) + { + return; + } + + Logging.Trace ($"targetView Accepting: {args?.Context?.Source?.Title}"); + eventSource.Add ($"targetView Accepting: {args?.Context?.Source?.Title}: "); + eventLog.MoveDown (); + }; + + targetView.FilePopoverMenu!.Accepted += (o, args) => + { + if (args.Cancel) + { + return; + } + + Logging.Trace ($"FilePopoverMenu Accepted: {args?.Context?.Source?.Text}"); + eventSource.Add ($"FilePopoverMenu Accepted: {args?.Context?.Source?.Text}: "); + eventLog.MoveDown (); + }; + + app.Add (eventLog); + + Application.Run (app); + app.Dispose (); + Application.Shutdown (); + } + + public class TargetView : View + { + internal PopoverMenu? FilePopoverMenu { get; } + + private CheckBox? _enableOverwriteCb; + private CheckBox? _autoSaveCb; + private CheckBox? _editModeCb; + + private RadioGroup? _mutuallyExclusiveOptionsRg; + + private ColorPicker? _menuBgColorCp; + + public TargetView () + { + CanFocus = true; + Text = "TargetView"; + BorderStyle = LineStyle.Dashed; + + AddCommand ( + Command.Context, + ctx => + { + FilePopoverMenu?.MakeVisible (); + + return true; + }); + + KeyBindings.Add (PopoverMenu.DefaultKey, Command.Context); + + MouseBindings.ReplaceCommands (PopoverMenu.MouseFlags, Command.Context); + + AddCommand ( + Command.Cancel, + ctx => + { + if (Application.Popover?.GetActivePopover () as PopoverMenu is { Visible: true } visiblePopover) + { + visiblePopover.Visible = false; + } + + return true; + }); + + MouseBindings.ReplaceCommands (MouseFlags.Button1Clicked, Command.Cancel); + + Label lastCommandLabel = new () + { + Title = "_Last Command:", + X = 15, + Y = 10, + }; + + View lastCommandText = new () + { + X = Pos.Right (lastCommandLabel) + 1, + Y = Pos.Top (lastCommandLabel), + Height = Dim.Auto (), + Width = Dim.Auto () + }; + + Add (lastCommandLabel, lastCommandText); + + AddCommand (Command.New, HandleCommand); + HotKeyBindings.Add (Key.F2, Command.New); + + AddCommand (Command.Open, HandleCommand); + HotKeyBindings.Add (Key.F3, Command.Open); + + AddCommand (Command.Save, HandleCommand); + HotKeyBindings.Add (Key.F4, Command.Save); + + AddCommand (Command.SaveAs, HandleCommand); + HotKeyBindings.Add (Key.A.WithCtrl, Command.SaveAs); + + HotKeyBindings.Add (Key.W.WithCtrl, Command.EnableOverwrite); + + var fileMenu = new Menuv2 + { + Id = "fileMenu" + }; + ConfigureFileMenu (fileMenu); + + var optionsSubMenu = new Menuv2 + { + Id = "optionsSubMenu", + Visible = false + }; + ConfigureOptionsSubMenu (optionsSubMenu); + + var optionsSubMenuItem = new MenuItemv2 (this, Command.NotBound, "O_ptions", "File options", optionsSubMenu); + fileMenu.Add (optionsSubMenuItem); + + var detailsSubMenu = new Menuv2 + { + Id = "detailsSubMenu", + Visible = false + }; + ConfigureDetialsSubMenu (detailsSubMenu); + + var detailsSubMenuItem = new MenuItemv2 (this, Command.NotBound, "_Details", "File details", detailsSubMenu); + fileMenu.Add (detailsSubMenuItem); + + var moreDetailsSubMenu = new Menuv2 + { + Id = "moreDetailsSubMenu", + Visible = false + }; + ConfigureMoreDetailsSubMenu (moreDetailsSubMenu); + + var moreDetailsSubMenuItem = new MenuItemv2 (this, Command.NotBound, "_More Details", "More details", moreDetailsSubMenu); + detailsSubMenu.Add (moreDetailsSubMenuItem); + + FilePopoverMenu = new (fileMenu) + { + Id = "FilePopoverMenu" + }; + + MenuBarItemv2 fileMenuRootItem = new ("_File", FilePopoverMenu); + + AddCommand (Command.Cut, HandleCommand); + HotKeyBindings.Add (Key.X.WithCtrl, Command.Cut); + + AddCommand (Command.Copy, HandleCommand); + HotKeyBindings.Add (Key.C.WithCtrl, Command.Copy); + + AddCommand (Command.Paste, HandleCommand); + HotKeyBindings.Add (Key.V.WithCtrl, Command.Paste); + + AddCommand (Command.SelectAll, HandleCommand); + HotKeyBindings.Add (Key.T.WithCtrl, Command.SelectAll); + + Add (new MenuBarv2 ( + [ + fileMenuRootItem, + new MenuBarItemv2 ( + "_Edit", + [ + new MenuItemv2 (this, Command.Cut), + new MenuItemv2 (this, Command.Copy), + new MenuItemv2 (this, Command.Paste), + new Line (), + new MenuItemv2 (this, Command.SelectAll) + ] + ), + new MenuBarItemv2 (this, Command.NotBound, "_Help") + { + Key = Key.F1, + Action = () => { MessageBox.Query ("Help", "This is the help...", "_Ok"); } + } + ] + ) + ); + + Label lastAcceptedLabel = new () + { + Title = "Last Accepted:", + X = Pos.Left (lastCommandLabel), + Y = Pos.Bottom (lastCommandLabel) + }; + + View lastAcceptedText = new () + { + X = Pos.Right (lastAcceptedLabel) + 1, + Y = Pos.Top (lastAcceptedLabel), + Height = Dim.Auto (), + Width = Dim.Auto () + }; + + Add (lastAcceptedLabel, lastAcceptedText); + + CheckBox autoSaveStatusCb = new () + { + Title = "AutoSave", + X = Pos.Left (lastAcceptedLabel), + Y = Pos.Bottom (lastAcceptedLabel) + }; + + autoSaveStatusCb.CheckedStateChanged += (sender, args) => { _autoSaveCb!.CheckedState = autoSaveStatusCb.CheckedState; }; + + Add (autoSaveStatusCb); + + CheckBox enableOverwriteStatusCb = new () + { + Title = "Enable Overwrite", + X = Pos.Left (autoSaveStatusCb), + Y = Pos.Bottom (autoSaveStatusCb) + }; + enableOverwriteStatusCb.CheckedStateChanged += (sender, args) => { _enableOverwriteCb!.CheckedState = enableOverwriteStatusCb.CheckedState; }; + base.Add (enableOverwriteStatusCb); + + AddCommand ( + Command.EnableOverwrite, + ctx => + { + enableOverwriteStatusCb.CheckedState = + enableOverwriteStatusCb.CheckedState == CheckState.UnChecked ? CheckState.Checked : CheckState.UnChecked; + + return HandleCommand (ctx); + }); + + CheckBox editModeStatusCb = new () + { + Title = "EditMode (App binding)", + X = Pos.Left (enableOverwriteStatusCb), + Y = Pos.Bottom (enableOverwriteStatusCb) + }; + editModeStatusCb.CheckedStateChanged += (sender, args) => { _editModeCb!.CheckedState = editModeStatusCb.CheckedState; }; + base.Add (editModeStatusCb); + + AddCommand (Command.Edit, ctx => + { + editModeStatusCb.CheckedState = + editModeStatusCb.CheckedState == CheckState.UnChecked ? CheckState.Checked : CheckState.UnChecked; + + return HandleCommand (ctx); + }); + + Application.KeyBindings.Add (Key.F9, this, Command.Edit); + + + FilePopoverMenu!.Accepted += (o, args) => + { + lastAcceptedText.Text = args?.Context?.Source?.Title!; + + if (args?.Context?.Source is MenuItemv2 mi && mi.CommandView == _autoSaveCb) + { + autoSaveStatusCb.CheckedState = _autoSaveCb.CheckedState; + } + }; + + FilePopoverMenu!.VisibleChanged += (sender, args) => + { + if (FilePopoverMenu!.Visible) + { + lastCommandText.Text = string.Empty; + } + }; + + Add ( + new Button + { + Title = "_Button", + X = Pos.Center (), + Y = Pos.Center () + }); + + autoSaveStatusCb.SetFocus (); + + return; + + // Add the commands supported by this View + bool? HandleCommand (ICommandContext? ctx) + { + lastCommandText.Text = ctx?.Command!.ToString ()!; + + return true; + } + } + + private void ConfigureFileMenu (Menuv2 menu) + { + var newFile = new MenuItemv2 + { + Command = Command.New, + TargetView = this + }; + + var openFile = new MenuItemv2 + { + Command = Command.Open, + TargetView = this + }; + + var saveFile = new MenuItemv2 + { + Command = Command.Save, + TargetView = this + }; + + var saveFileAs = new MenuItemv2 (this, Command.SaveAs); + + menu.Add (newFile, openFile, saveFile, saveFileAs, new Line ()); + } + + private void ConfigureOptionsSubMenu (Menuv2 menu) + { + // This is an example of a menu item with a checkbox that is NOT + // bound to a Command. The PopoverMenu will raise Accepted when Alt-U is pressed. + // The checkbox state will automatically toggle each time Alt-U is pressed beacuse + // the MenuItem actaully gets the key events. + var autoSave = new MenuItemv2 + { + Title = "_Auto Save", + Text = "(no Command)", + Key = Key.F10 + }; + + autoSave.CommandView = _autoSaveCb = new () + { + Title = autoSave.Title, + HighlightStyle = HighlightStyle.None, + CanFocus = false + }; + + // This is an example of a MenuItem with a checkbox that is bound to a command. + // When the key bound to Command.EntableOverwrite is pressed, InvokeCommand will invoke it + // on targetview, and thus the MenuItem will never see the key event. + // Because of this, the check box will not automatically track the state. + var enableOverwrite = new MenuItemv2 + { + Title = "Enable _Overwrite", + Text = "Overwrite", + Command = Command.EnableOverwrite, + TargetView = this + }; + + enableOverwrite.CommandView = _enableOverwriteCb = new () + { + Title = enableOverwrite.Title, + HighlightStyle = HighlightStyle.None, + CanFocus = false + }; + + _enableOverwriteCb.Accepting += (sender, args) => args.Cancel = true; + + var mutuallyExclusiveOptions = new MenuItemv2 + { + HelpText = "3 Mutually Exclusive Options", + Key = Key.F7 + }; + + mutuallyExclusiveOptions.CommandView = _mutuallyExclusiveOptionsRg = new RadioGroup () + { + RadioLabels = [ "G_ood", "_Bad", "U_gly" ] + }; + + var menuBGColor = new MenuItemv2 + { + HelpText = "Menu BG Color", + Key = Key.F8, + }; + + menuBGColor.CommandView = _menuBgColorCp = new ColorPicker() + { + Width = 30 + }; + + _menuBgColorCp.ColorChanged += (sender, args) => + { + menu.ColorScheme = menu.ColorScheme with + { + Normal = new (menu.ColorScheme.Normal.Foreground, args.CurrentValue) + }; + }; + + menu.Add (autoSave, enableOverwrite, new Line (), mutuallyExclusiveOptions, new Line (), menuBGColor); + } + + private void ConfigureDetialsSubMenu (Menuv2 menu) + { + var shortcut2 = new MenuItemv2 + { + Title = "_Detail 1", + Text = "Some detail #1" + }; + + var shortcut3 = new MenuItemv2 + { + Title = "_Three", + Text = "The 3rd item" + }; + + var editMode = new MenuItemv2 + { + Title = "E_dit Mode", + Text = "App binding to Command.Edit", + Command = Command.Edit, + }; + + editMode.CommandView = _editModeCb = new CheckBox + { + Title = editMode.Title, + HighlightStyle = HighlightStyle.None, + CanFocus = false + }; + + // This ensures the checkbox state toggles when the hotkey of Title is pressed. + //shortcut4.Accepting += (sender, args) => args.Cancel = true; + + menu.Add (shortcut2, shortcut3, new Line (), editMode); + } + + private void ConfigureMoreDetailsSubMenu (Menuv2 menu) + { + var deeperDetail = new MenuItemv2 + { + Title = "_Deeper Detail", + Text = "Deeper Detail", + Action = () => { MessageBox.Query ("Deeper Detail", "Lots of details", "_Ok"); } + }; + + var shortcut4 = new MenuItemv2 + { + Title = "_Third", + Text = "Below the line" + }; + + // This ensures the checkbox state toggles when the hotkey of Title is pressed. + //shortcut4.Accepting += (sender, args) => args.Cancel = true; + + menu.Add (deeperDetail, new Line (), shortcut4); + } + + /// + protected override void Dispose (bool disposing) + { + if (disposing) + { + // if (FilePopoverMenu is { }) + // { + // FilePopoverMenu.Visible = false; + // FilePopoverMenu?.Dispose (); + // FilePopoverMenu = null; + // } + } + + base.Dispose (disposing); + } + } + + private const string LOGFILE_LOCATION = "./logs"; + private static readonly string _logFilePath = string.Empty; + private static readonly LoggingLevelSwitch _logLevelSwitch = new (); + + private static ILogger CreateLogger () + { + // Configure Serilog to write logs to a file + _logLevelSwitch.MinimumLevel = LogEventLevel.Verbose; + + Log.Logger = new LoggerConfiguration () + .MinimumLevel.ControlledBy (_logLevelSwitch) + .Enrich.FromLogContext () // Enables dynamic enrichment + .WriteTo.Debug () + .WriteTo.File ( + _logFilePath, + rollingInterval: RollingInterval.Day, + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}") + .CreateLogger (); + + // Create a logger factory compatible with Microsoft.Extensions.Logging + using ILoggerFactory loggerFactory = LoggerFactory.Create ( + builder => + { + builder + .AddSerilog (dispose: true) // Integrate Serilog with ILogger + .SetMinimumLevel (LogLevel.Trace); // Set minimum log level + }); + + // Get an ILogger instance + return loggerFactory.CreateLogger ("Global Logger"); + } +} diff --git a/UICatalog/Scenarios/Snake.cs b/UICatalog/Scenarios/Snake.cs index 1896c7dae8..3ff04241d3 100644 --- a/UICatalog/Scenarios/Snake.cs +++ b/UICatalog/Scenarios/Snake.cs @@ -317,7 +317,7 @@ public SnakeView (SnakeState state) protected override bool OnDrawingContent () { SetAttribute (white); - ClearViewport (); + ClearViewport (null); var canvas = new LineCanvas (); diff --git a/UICatalog/Scenarios/Transparent.cs b/UICatalog/Scenarios/Transparent.cs index 5841ed90fa..5e87b56543 100644 --- a/UICatalog/Scenarios/Transparent.cs +++ b/UICatalog/Scenarios/Transparent.cs @@ -67,7 +67,7 @@ public class TransparentView : FrameView public TransparentView () { Title = "Transparent View"; - base.Text = "View.Text.\nThis should be opaque.\nNote how clipping works?"; + //base.Text = "View.Text.\nThis should be opaque.\nNote how clipping works?"; TextFormatter.Alignment = Alignment.Center; TextFormatter.VerticalAlignment = Alignment.Center; Arrangement = ViewArrangement.Overlapped | ViewArrangement.Resizable | ViewArrangement.Movable; @@ -85,18 +85,33 @@ public TransparentView () Height = 8, BorderStyle = LineStyle.Dashed, Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable, - ShadowStyle = ShadowStyle.Transparent, + // ShadowStyle = ShadowStyle.Transparent, }; transparentSubView.Border!.Thickness = new (1, 1, 1, 1); transparentSubView.ColorScheme = Colors.ColorSchemes ["Dialog"]; + transparentSubView.Visible = false; Button button = new Button () { Title = "_Opaque Shadows No Worky", X = Pos.Center (), - Y = 4, + Y = 2, ColorScheme = Colors.ColorSchemes ["Dialog"], }; + button.Visible = false; + + + var shortcut = new Shortcut () + { + Id = "shortcut", + X = Pos.Center (), + Y = Pos.AnchorEnd(), + Title = "A _Shortcut", + HelpText = "Help!", + Key = Key.F11, + ColorScheme = Colors.ColorSchemes ["Base"] + + }; button.ClearingViewport += (sender, args) => { @@ -105,6 +120,7 @@ public TransparentView () base.Add (button); + base.Add (shortcut); base.Add (transparentSubView); } diff --git a/UICatalog/Scenarios/ViewExperiments.cs b/UICatalog/Scenarios/ViewExperiments.cs index 8126ad6b01..c510195725 100644 --- a/UICatalog/Scenarios/ViewExperiments.cs +++ b/UICatalog/Scenarios/ViewExperiments.cs @@ -56,6 +56,50 @@ public override void Main () Title = $"TopButton _{GetNextHotKey ()}", }; + var popoverView = new View () + { + X = Pos.Center (), + Y = Pos.Center (), + Width = 30, + Height = 10, + Title = "Popover", + Text = "This is a popover", + Visible = false, + CanFocus = true, + Arrangement = ViewArrangement.Resizable | ViewArrangement.Movable + }; + popoverView.BorderStyle = LineStyle.RoundedDotted; + + Button popoverButton = new () + { + X = Pos.Center (), + Y = Pos.Center (), + Title = $"_Close", + }; + //popoverButton.Accepting += (sender, e) => Application.Popover!.Visible = false; + popoverView.Add (popoverButton); + + button.Accepting += ButtonAccepting; + + void ButtonAccepting (object sender, CommandEventArgs e) + { + //Application.Popover = popoverView; + //Application.Popover!.Visible = true; + } + + testFrame.MouseClick += TestFrameOnMouseClick; + + void TestFrameOnMouseClick (object sender, MouseEventArgs e) + { + if (e.Flags == MouseFlags.Button3Clicked) + { + popoverView.X = e.ScreenPosition.X; + popoverView.Y = e.ScreenPosition.Y; + //Application.Popover = popoverView; + //Application.Popover!.Visible = true; + } + } + testFrame.Add (button); editor.AutoSelectViewToEdit = true; @@ -63,6 +107,7 @@ public override void Main () editor.AutoSelectAdornments = true; Application.Run (app); + popoverView.Dispose (); app.Dispose (); Application.Shutdown (); @@ -70,6 +115,7 @@ public override void Main () return; } + private int _hotkeyCount; private char GetNextHotKey () diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index 4d4ed17d64..4334aabb41 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -74,7 +74,7 @@ public class UICatalogApp private static Options _options; private static ObservableCollection? _scenarios; - private const string LOGFILE_LOCATION = "./logs"; + private const string LOGFILE_LOCATION = "logs"; private static string _logFilePath = string.Empty; private static readonly LoggingLevelSwitch _logLevelSwitch = new (); @@ -171,7 +171,7 @@ private static int Main (string [] args) resultsFile.AddAlias ("--f"); // what's the app name? - _logFilePath = $"{LOGFILE_LOCATION}/{Assembly.GetExecutingAssembly ().GetName ().Name}.log"; + _logFilePath = $"{LOGFILE_LOCATION}/{Assembly.GetExecutingAssembly ().GetName ().Name}"; Option debugLogLevel = new Option ("--debug-log-level", $"The level to use for logging (debug console and {_logFilePath})").FromAmong ( Enum.GetNames () ); @@ -278,7 +278,7 @@ private static ILogger CreateLogger () return loggerFactory.CreateLogger ("Global Logger"); } - private static void OpenUrl (string url) + public static void OpenUrl (string url) { if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { @@ -690,7 +690,7 @@ private static void VerifyObjectsWereDisposed () return; } - // Validate there are no outstanding Responder-based instances + // Validate there are no outstanding View instances // after a scenario was selected to run. This proves the main UI Catalog // 'app' closed cleanly. foreach (View? inst in View.Instances) @@ -1354,11 +1354,14 @@ private List CreateLoggingMenuItems () menuItems.Add (null!); menuItems.Add ( - new () - { - Title = $"Log file: {_logFilePath}" - //CanExecute = () => false - }); + new ( + $"_Open Log Folder", + "", + () => OpenUrl (LOGFILE_LOCATION), + null, + null, + null + )); return menuItems.ToArray ()!; } diff --git a/docfx/docs/Popovers.md b/docfx/docs/Popovers.md new file mode 100644 index 0000000000..74ca7e864f --- /dev/null +++ b/docfx/docs/Popovers.md @@ -0,0 +1,18 @@ +# Popovers Deep Dive + +Normally Views cannot draw outside of their `Viewport`. Options for influencing content outside of the `Viewport` include: + +1) Modifying the `Border` behavior +2) Modifying the `Margin` behavior +3) Using @Terminal.Gui.Application.Popover + +Popovers are useful for scenarios such as menus, autocomplete popups, and drop-down combo boxes. + +A Popover is any View that meets these characteristics" + +- Implements the @Terminal.Gui.IPopover interface +- Is Focusable (`CetFocus = true`) +- Is Transparent (`ViewportSettings = ViewportSettings.Transparent | ViewportSettings.TransparentMouse` +- Sets `Visible = false` when it receives `Application.QuitKey` + +@Terminal.Gui.PopoverMenu provides a sophisticated implementation. \ No newline at end of file diff --git a/docfx/docs/index.md b/docfx/docs/index.md index 2c3cff4db0..6091ea18cd 100644 --- a/docfx/docs/index.md +++ b/docfx/docs/index.md @@ -33,6 +33,7 @@ See [What's New in V2 For more](newinv2.md). * [Mouse API](mouse.md) * [Multi-tasking and the Application Main Loop](mainloop.md) * [Navigation](navigation.md) +* [Popovers](Popovers.md) * [View Deep Dive](View.md) * [Views](views.md) * [Scrolling Deep Dive](scrolling.md) diff --git a/docfx/docs/logging.md b/docfx/docs/logging.md index 6df97a0feb..a175591ed2 100644 --- a/docfx/docs/logging.md +++ b/docfx/docs/logging.md @@ -1,14 +1,20 @@ # Logging -Logging has come to Terminal.Gui! You can now enable comprehensive logging of the internals of the libray. This can help diagnose issues with specific terminals, keyboard cultures and/or operating system specific issues. +Logging has come to Terminal.Gui! You can now enable comprehensive logging of the internals of the library. This can help diagnose issues with specific terminals, keyboard cultures and/or operating system specific issues. -To enable file logging you should set the static property `Logging.Logger` to an instance of `Microsoft.Extensions.Logging.ILogger`. If your program already uses logging you can provide a shared instance or instance from Dependency Injection (DI). +To enable file logging you should set the static property `Logging.Logger` to an instance of `Microsoft.Extensions.Logging.ILogger`. If your program already uses logging you can provide a shared instance or instance from Dependency Injection (DI). Alternatively you can create a new log to ensure only Terminal.Gui logs appear. -Any logging framework will work (Serilog, NLog, Log4Net etc) but you should ensure you only log to File or UDP etc (i.e. not to console!). +Any logging framework will work (Serilog, NLog, Log4Net etc) but you should ensure you don't log to the stdout console (File, Debug Output, or UDP etc... are all fine). -## Worked example with Serilog to file +## UICatalog + +UI Catalog has built-in UI for logging. It logs to both the debug console and a file. By default it only logs at the `Warning` level. + +![UICatalog Logging](../images/UICatalog_Logging.png) + +## Example with Serilog to file Here is an example of how to add logging of Terminal.Gui internals to your program using Serilog file log. @@ -81,7 +87,7 @@ Example logs: ## Metrics -If you are finding that the UI is slow or unresponsive - or are just interested in performance metrics. You can see these by instaling the `dotnet-counter` tool and running it for your process. +If you are finding that the UI is slow or unresponsive - or are just interested in performance metrics. You can see these by installing the `dotnet-counter` tool and running it for your process. ``` dotnet tool install dotnet-counters --global diff --git a/docfx/docs/toc.yml b/docfx/docs/toc.yml index 6304b7e7db..9c6a6cea6a 100644 --- a/docfx/docs/toc.yml +++ b/docfx/docs/toc.yml @@ -30,6 +30,8 @@ href: mainloop.md - name: Navigation href: navigation.md +- name: Popovers + href: Popovers.md - name: View Deep Dive href: View.md - name: View List diff --git a/docfx/images/UICatalog_Logging.png b/docfx/images/UICatalog_Logging.png new file mode 100644 index 0000000000000000000000000000000000000000..55377ca866628505b6b828f2ccdd137c326e3968 GIT binary patch literal 53046 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 literal 0 HcmV?d00001 From ac5c7e5d9e5b74e232a2861928805f7e3571b2ad Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 31 Mar 2025 17:12:55 -0600 Subject: [PATCH 04/28] Partially Fixes #2975 - Replaces old `ContextMenu` with new `Bar/Shortcut` based implementation (#4008) * touching publish.yml * Nuked ContextMenuv2 - use PopverMenu instead * WIP context menu stuff * More robust dispose * Removed ConextMenu; use PopoverMenu instead * Code cleanup * Code cleanup2 --- .../Application/Application.Initialization.cs | 3 + Terminal.Gui/Application/Application.Run.cs | 2 +- Terminal.Gui/Application/Application.cs | 1 + .../Application/ApplicationPopover.cs | 68 +- Terminal.Gui/Application/PopoverBaseImpl.cs | 32 +- .../ConsoleDrivers/V2/ApplicationV2.cs | 4 + .../Drawing/Color/ColorScheme.Colors.cs | 2 +- Terminal.Gui/Resources/config.json | 2 +- Terminal.Gui/Terminal.Gui.csproj | 2 +- Terminal.Gui/Views/CharMap/CharMap.cs | 42 +- Terminal.Gui/Views/FileDialog.cs | 195 +- Terminal.Gui/Views/Menu/ContextMenu.cs | 283 --- Terminal.Gui/Views/Menu/ContextMenuv2.cs | 104 - Terminal.Gui/Views/Menu/MenuBarItemv2.cs | 2 +- Terminal.Gui/Views/Menu/MenuBarv2.cs | 174 +- Terminal.Gui/Views/Menu/MenuItemv2.cs | 21 +- Terminal.Gui/Views/Menu/Menuv2.cs | 58 +- Terminal.Gui/Views/Menu/PopoverMenu.cs | 172 +- Terminal.Gui/Views/{Menu => Menuv1}/Menu.cs | 3 +- .../Views/{Menu => Menuv1}/MenuBar.cs | 1 - .../Views/{Menu => Menuv1}/MenuBarItem.cs | 0 .../{Menu => Menuv1}/MenuClosingEventArgs.cs | 0 .../Views/{Menu => Menuv1}/MenuItem.cs | 2 +- .../{Menu => Menuv1}/MenuItemCheckStyle.cs | 0 .../{Menu => Menuv1}/MenuOpenedEventArgs.cs | 0 .../{Menu => Menuv1}/MenuOpeningEventArgs.cs | 0 Terminal.Gui/Views/TextField.cs | 10 +- Terminal.Gui/Views/TextView.cs | 22 +- TerminalGuiFluentTesting/GuiTestContext.cs | 42 +- .../FluentTests/BasicFluentAssertionTests.cs | 56 +- .../Application/ApplicationPopoverTests.cs | 422 +--- Tests/UnitTests/Views/ContextMenuTests.cs | 2218 ----------------- Tests/UnitTests/Views/TextViewTests.cs | 2 +- .../Application/ApplicationPopoverTests.cs | 18 +- UICatalog/Scenarios/ContextMenus.cs | 4 +- UICatalog/Scenarios/MenusV2.cs | 18 - UICatalog/Scenarios/Notepad.cs | 44 +- UICatalog/Scenarios/TableEditor.cs | 65 +- UICatalog/Scenarios/TreeViewFileSystem.cs | 326 ++- docfx/docs/migratingfromv1.md | 18 + 40 files changed, 710 insertions(+), 3728 deletions(-) delete mode 100644 Terminal.Gui/Views/Menu/ContextMenu.cs delete mode 100644 Terminal.Gui/Views/Menu/ContextMenuv2.cs rename Terminal.Gui/Views/{Menu => Menuv1}/Menu.cs (99%) rename Terminal.Gui/Views/{Menu => Menuv1}/MenuBar.cs (99%) rename Terminal.Gui/Views/{Menu => Menuv1}/MenuBarItem.cs (100%) rename Terminal.Gui/Views/{Menu => Menuv1}/MenuClosingEventArgs.cs (100%) rename Terminal.Gui/Views/{Menu => Menuv1}/MenuItem.cs (99%) rename Terminal.Gui/Views/{Menu => Menuv1}/MenuItemCheckStyle.cs (100%) rename Terminal.Gui/Views/{Menu => Menuv1}/MenuOpenedEventArgs.cs (100%) rename Terminal.Gui/Views/{Menu => Menuv1}/MenuOpeningEventArgs.cs (100%) delete mode 100644 Tests/UnitTests/Views/ContextMenuTests.cs diff --git a/Terminal.Gui/Application/Application.Initialization.cs b/Terminal.Gui/Application/Application.Initialization.cs index 7093b87025..30f2f58216 100644 --- a/Terminal.Gui/Application/Application.Initialization.cs +++ b/Terminal.Gui/Application/Application.Initialization.cs @@ -82,7 +82,10 @@ internal static void InternalInit ( ResetState (ignoreDisposed: true); } + Debug.Assert (Navigation is null); Navigation = new (); + + Debug.Assert(Popover is null); Popover = new (); // For UnitTests diff --git a/Terminal.Gui/Application/Application.Run.cs b/Terminal.Gui/Application/Application.Run.cs index 4412f5d233..f4a4cf44e6 100644 --- a/Terminal.Gui/Application/Application.Run.cs +++ b/Terminal.Gui/Application/Application.Run.cs @@ -564,7 +564,7 @@ public static void End (RunState runState) { ArgumentNullException.ThrowIfNull (runState); - Popover?.HidePopover (Popover?.GetActivePopover ()); + Popover?.Hide (Popover?.GetActivePopover ()); runState.Toplevel.OnUnloaded (); diff --git a/Terminal.Gui/Application/Application.cs b/Terminal.Gui/Application/Application.cs index 5d01106ae4..07bec47c75 100644 --- a/Terminal.Gui/Application/Application.cs +++ b/Terminal.Gui/Application/Application.cs @@ -153,6 +153,7 @@ internal static void ResetState (bool ignoreDisposed = false) { popover.Visible = false; } + Popover?.Dispose (); Popover = null; TopLevels.Clear (); diff --git a/Terminal.Gui/Application/ApplicationPopover.cs b/Terminal.Gui/Application/ApplicationPopover.cs index 9c0b944623..bc4a0e0102 100644 --- a/Terminal.Gui/Application/ApplicationPopover.cs +++ b/Terminal.Gui/Application/ApplicationPopover.cs @@ -1,13 +1,12 @@ #nullable enable -using System.Diagnostics; - namespace Terminal.Gui; /// -/// Helper class for support of views for . Held by +/// Helper class for support of views for . Held by +/// /// -public class ApplicationPopover +public sealed class ApplicationPopover : IDisposable { /// /// Initializes a new instance of the class. @@ -16,27 +15,41 @@ public ApplicationPopover () { } private readonly List _popovers = []; - /// + /// + /// Gets the list of popovers registered with the application. + /// public IReadOnlyCollection Popovers => _popovers.AsReadOnly (); /// /// Registers with the application. - /// This enables the popover to receive keyboard events even when when it is not active. + /// This enables the popover to receive keyboard events even when it is not active. /// + /// + /// When a popover is registered, the View instance lifetime is managed by the application. Call + /// + /// to manage the lifetime of the popover directly. + /// /// - public void Register (IPopover? popover) + /// , after it has been registered. + public IPopover? Register (IPopover? popover) { if (popover is { } && !_popovers.Contains (popover)) { _popovers.Add (popover); - } + + return popover; } /// /// De-registers with the application. Use this to remove the popover and it's /// keyboard bindings from the application. /// + /// + /// When a popover is registered, the View instance lifetime is managed by the application. Call + /// + /// to manage the lifetime of the popover directly. + /// /// /// public bool DeRegister (IPopover? popover) @@ -61,22 +74,25 @@ public bool DeRegister (IPopover? popover) /// /// Gets the active popover, if any. /// + /// + /// Note, the active pop over does not necessarily to be registered with the application. + /// /// public IPopover? GetActivePopover () { return _activePopover; } /// - /// Shows . IPopover implementations should use OnVisibleChnaged/VisibleChanged to be + /// Shows . IPopover implementations should use OnVisibleChanaged/VisibleChanged to be /// notified when the user has done something to cause the popover to be hidden. /// /// /// - /// Note, this API calls . To disable the popover from processing keyboard events, + /// This API calls . To disable the popover from processing keyboard events, /// either call to /// remove the popover from the application or set to . /// /// /// - public void ShowPopover (IPopover? popover) + public void Show (IPopover? popover) { // If there's an existing popover, hide it. if (_activePopover is View popoverView) @@ -87,8 +103,6 @@ public void ShowPopover (IPopover? popover) if (popover is View newPopover) { - Register (popover); - if (!newPopover.IsInitialized) { newPopover.BeginInit (); @@ -103,10 +117,11 @@ public void ShowPopover (IPopover? popover) /// /// Causes the specified popover to be hidden. - /// If the popover is dervied from , this is the same as setting to . + /// If the popover is dervied from , this is the same as setting + /// to . /// /// - public void HidePopover (IPopover? popover) + public void Hide (IPopover? popover) { // If there's an existing popover, hide it. if (_activePopover is View popoverView && popoverView == popover) @@ -117,7 +132,6 @@ public void HidePopover (IPopover? popover) } } - /// /// Called when the user presses a key. Dispatches the key to the active popover, if any, /// otherwise to the popovers in the order they were registered. Inactive popovers only get hotkeys. @@ -127,9 +141,11 @@ public void HidePopover (IPopover? popover) internal bool DispatchKeyDown (Key key) { // Do active first - Active gets all key down events. - if (GetActivePopover () as View is { Visible: true } visiblePopover) + var activePopover = GetActivePopover () as View; + + if (activePopover is { Visible: true }) { - if (visiblePopover.NewKeyDownEvent (key)) + if (activePopover.NewKeyDownEvent (key)) { return true; } @@ -141,7 +157,7 @@ internal bool DispatchKeyDown (Key key) foreach (IPopover popover in _popovers) { - if (GetActivePopover () == popover || popover is not View popoverView) + if (popover == activePopover || popover is not View popoverView) { continue; } @@ -157,4 +173,18 @@ internal bool DispatchKeyDown (Key key) return hotKeyHandled is true; } + + /// + public void Dispose () + { + foreach (IPopover popover in _popovers) + { + if (popover is View view) + { + view.Dispose (); + } + } + + _popovers.Clear (); + } } diff --git a/Terminal.Gui/Application/PopoverBaseImpl.cs b/Terminal.Gui/Application/PopoverBaseImpl.cs index fce7d78660..64b90532ce 100644 --- a/Terminal.Gui/Application/PopoverBaseImpl.cs +++ b/Terminal.Gui/Application/PopoverBaseImpl.cs @@ -6,19 +6,18 @@ namespace Terminal.Gui; /// /// /// -/// To show a Popover, use . To hide a popover, -/// call with set to . +/// To show a Popover, use . To hide a popover, +/// call with set to . /// /// -/// If the user clicks anywhere not occulded by a SubView of the Popover, presses , +/// If the user clicks anywhere not occluded by a SubView of the Popover, presses , /// or causes another popover to show, the Popover will be hidden. /// /// - public abstract class PopoverBaseImpl : View, IPopover { /// - /// + /// Creates a new PopoverBaseImpl. /// protected PopoverBaseImpl () { @@ -28,10 +27,10 @@ protected PopoverBaseImpl () Height = Dim.Fill (); ViewportSettings = ViewportSettings.Transparent | ViewportSettings.TransparentMouse; - //// TODO: Add a diagnostic setting for this? - TextFormatter.VerticalAlignment = Alignment.End; - TextFormatter.Alignment = Alignment.End; - base.Text = "popover"; + // TODO: Add a diagnostic setting for this? + //TextFormatter.VerticalAlignment = Alignment.End; + //TextFormatter.Alignment = Alignment.End; + //base.Text = "popover"; AddCommand (Command.Quit, Quit); KeyBindings.Add (Application.QuitKey, Command.Quit); @@ -55,24 +54,13 @@ protected PopoverBaseImpl () protected override bool OnVisibleChanging () { bool ret = base.OnVisibleChanging (); - if (!ret & !Visible) + if (!ret && !Visible) { - // Whenvver visible is changing to true, we need to resize; + // Whenever visible is changing to true, we need to resize; // it's our only chance because we don't get laid out until we're visible Layout (Application.Screen.Size); } return ret; } - - // TODO: Pretty sure this is not needed. set_Visible SetFocus already - ///// - //protected override void OnVisibleChanged () - //{ - // base.OnVisibleChanged (); - // if (Visible) - // { - // //SetFocus (); - // } - //} } diff --git a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs index 9baeba301f..e6461b1443 100644 --- a/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs +++ b/Terminal.Gui/ConsoleDrivers/V2/ApplicationV2.cs @@ -1,5 +1,6 @@ #nullable enable using System.Collections.Concurrent; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; @@ -63,7 +64,10 @@ public override void Init (IConsoleDriver? driver = null, string? driverName = n _driverName = driverName; } + Debug.Assert(Application.Navigation is null); Application.Navigation = new (); + + Debug.Assert (Application.Popover is null); Application.Popover = new (); Application.AddKeyBindings (); diff --git a/Terminal.Gui/Drawing/Color/ColorScheme.Colors.cs b/Terminal.Gui/Drawing/Color/ColorScheme.Colors.cs index 5b8b130fdf..a311032de1 100644 --- a/Terminal.Gui/Drawing/Color/ColorScheme.Colors.cs +++ b/Terminal.Gui/Drawing/Color/ColorScheme.Colors.cs @@ -44,7 +44,7 @@ static Colors () /// /// Menu /// - /// The menu color scheme; used for , , and + /// The menu color scheme; used for , , and /// . /// /// diff --git a/Terminal.Gui/Resources/config.json b/Terminal.Gui/Resources/config.json index 31752d43b2..230533b290 100644 --- a/Terminal.Gui/Resources/config.json +++ b/Terminal.Gui/Resources/config.json @@ -33,7 +33,7 @@ // --------------- View Specific Settings --------------- - "ContextMenu.DefaultKey": "Shift+F10", + "PopoverMenu.DefaultKey": "Shift+F10", "FileDialog.MaxSearchResults": 10000, "FileDialogStyle.DefaultUseColors": false, "FileDialogStyle.DefaultUseUnicodeCharacters": false, diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj index d8efda92d5..67444b57c2 100644 --- a/Terminal.Gui/Terminal.Gui.csproj +++ b/Terminal.Gui/Terminal.Gui.csproj @@ -81,7 +81,7 @@ - + diff --git a/Terminal.Gui/Views/CharMap/CharMap.cs b/Terminal.Gui/Views/CharMap/CharMap.cs index e15a732dc2..a6753dfa7c 100644 --- a/Terminal.Gui/Views/CharMap/CharMap.cs +++ b/Terminal.Gui/Views/CharMap/CharMap.cs @@ -18,8 +18,6 @@ public class CharMap : View, IDesignable private const int HEADER_HEIGHT = 1; // Height of the header private int _rowHeight = 1; // Height of each row of 16 glyphs - changing this is not tested - private ContextMenu _contextMenu = new (); - /// /// Initializes a new instance. /// @@ -58,7 +56,7 @@ public CharMap () KeyBindings.Add (Key.PageDown, Command.PageDown); KeyBindings.Add (Key.Home, Command.Start); KeyBindings.Add (Key.End, Command.End); - KeyBindings.Add (ContextMenu.DefaultKey, Command.Context); + KeyBindings.Add (PopoverMenu.DefaultKey, Command.Context); MouseBindings.Add (MouseFlags.Button1DoubleClicked, Command.Accept); MouseBindings.ReplaceCommands (MouseFlags.Button3Clicked, Command.Context); @@ -505,32 +503,20 @@ public static string ToCamelCase (string str) SelectedCodePoint = newCodePoint; - _contextMenu = new () - { - Position = ViewportToScreen (GetCursor (SelectedCodePoint)) - }; + // This demonstrates how to create an ephemeral Popover; one that exists + // ony as long as the popover is visible. + // Note, for ephemeral Popovers, hotkeys are not supported. + PopoverMenu? contextMenu = new ( + [ + new (Strings.charMapCopyGlyph, string.Empty, CopyGlyph), + new (Strings.charMapCopyCP, string.Empty, CopyCodePoint) + ]); + + // Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused + // and the context menu is disposed when it is closed. + Application.Popover?.Register (contextMenu); - MenuBarItem menuItems = new ( - [ - new ( - Strings.charMapCopyGlyph, - "", - CopyGlyph, - null, - null, - (KeyCode)Key.G.WithCtrl - ), - new ( - Strings.charMapCopyCP, - "", - CopyCodePoint, - null, - null, - (KeyCode)Key.P.WithCtrl - ) - ] - ); - _contextMenu.Show (menuItems); + contextMenu?.MakeVisible (ViewportToScreen (GetCursor (SelectedCodePoint))); return true; } diff --git a/Terminal.Gui/Views/FileDialog.cs b/Terminal.Gui/Views/FileDialog.cs index b21da3d12c..cdeb939a82 100644 --- a/Terminal.Gui/Views/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialog.cs @@ -70,14 +70,15 @@ internal FileDialog (IFileSystem fileSystem) Canceled = true; _fileSystem = fileSystem; - Style = new FileDialogStyle (fileSystem); + Style = new (fileSystem); - _btnOk = new Button + _btnOk = new() { X = Pos.Align (Alignment.End, AlignmentModes.AddSpaceBetweenItems, alignmentGroupComplete), Y = Pos.AnchorEnd (), IsDefault = true, Text = Style.OkButtonText }; + _btnOk.Accepting += (s, e) => { if (e.Cancel) @@ -88,8 +89,7 @@ internal FileDialog (IFileSystem fileSystem) Accept (true); }; - - _btnCancel = new Button + _btnCancel = new() { X = Pos.Align (Alignment.End, AlignmentModes.AddSpaceBetweenItems, alignmentGroupComplete), Y = Pos.AnchorEnd (), @@ -97,30 +97,31 @@ internal FileDialog (IFileSystem fileSystem) }; _btnCancel.Accepting += (s, e) => - { - if (e.Cancel) - { - return; - } - if (Modal) - { - Application.RequestStop (); - } - }; - - _btnUp = new Button { X = 0, Y = 1, NoPadding = true }; + { + if (e.Cancel) + { + return; + } + + if (Modal) + { + Application.RequestStop (); + } + }; + + _btnUp = new() { X = 0, Y = 1, NoPadding = true }; _btnUp.Text = GetUpButtonText (); _btnUp.Accepting += (s, e) => _history.Up (); - _btnBack = new Button { X = Pos.Right (_btnUp) + 1, Y = 1, NoPadding = true }; + _btnBack = new() { X = Pos.Right (_btnUp) + 1, Y = 1, NoPadding = true }; _btnBack.Text = GetBackButtonText (); _btnBack.Accepting += (s, e) => _history.Back (); - _btnForward = new Button { X = Pos.Right (_btnBack) + 1, Y = 1, NoPadding = true }; + _btnForward = new() { X = Pos.Right (_btnBack) + 1, Y = 1, NoPadding = true }; _btnForward.Text = GetForwardButtonText (); _btnForward.Accepting += (s, e) => _history.Forward (); - _tbPath = new TextField { Width = Dim.Fill (), CaptionColor = new Color (Color.Black) }; + _tbPath = new() { Width = Dim.Fill (), CaptionColor = new (Color.Black) }; _tbPath.KeyDown += (s, k) => { @@ -134,12 +135,12 @@ internal FileDialog (IFileSystem fileSystem) _tbPath.Autocomplete = new AppendAutocomplete (_tbPath); _tbPath.Autocomplete.SuggestionGenerator = new FilepathSuggestionGenerator (); - _splitContainer = new TileView + _splitContainer = new() { X = 0, Y = Pos.Bottom (_btnBack), Width = Dim.Fill (), - Height = Dim.Fill (Dim.Func (() => IsInitialized ? _btnOk.Frame.Height : 1)), + Height = Dim.Fill (Dim.Func (() => IsInitialized ? _btnOk.Frame.Height : 1)) }; Initialized += (s, e) => @@ -150,7 +151,7 @@ internal FileDialog (IFileSystem fileSystem) // this.splitContainer.Border.BorderStyle = BorderStyle.None; - _tableView = new TableView + _tableView = new() { Width = Dim.Fill (), Height = Dim.Fill (), @@ -178,7 +179,7 @@ internal FileDialog (IFileSystem fileSystem) typeStyle.MinWidth = 6; typeStyle.ColorGetter = ColorGetter; - _treeView = new TreeView { Width = Dim.Fill (), Height = Dim.Fill () }; + _treeView = new() { Width = Dim.Fill (), Height = Dim.Fill () }; var fileDialogTreeBuilder = new FileSystemTreeBuilder (); _treeView.TreeBuilder = fileDialogTreeBuilder; @@ -190,31 +191,33 @@ internal FileDialog (IFileSystem fileSystem) _splitContainer.Tiles.ElementAt (0).ContentView.Add (_treeView); _splitContainer.Tiles.ElementAt (1).ContentView.Add (_tableView); - _btnToggleSplitterCollapse = new Button + _btnToggleSplitterCollapse = new() { X = Pos.Align (Alignment.Start, AlignmentModes.AddSpaceBetweenItems, alignmentGroupInput), Y = Pos.AnchorEnd (), Text = GetToggleSplitterText (false) }; _btnToggleSplitterCollapse.Accepting += (s, e) => - { - Tile tile = _splitContainer.Tiles.ElementAt (0); + { + Tile tile = _splitContainer.Tiles.ElementAt (0); - bool newState = !tile.ContentView.Visible; - tile.ContentView.Visible = newState; - _btnToggleSplitterCollapse.Text = GetToggleSplitterText (newState); - SetNeedsLayout (); - }; + bool newState = !tile.ContentView.Visible; + tile.ContentView.Visible = newState; + _btnToggleSplitterCollapse.Text = GetToggleSplitterText (newState); + SetNeedsLayout (); + }; - _tbFind = new TextField + _tbFind = new() { X = Pos.Align (Alignment.Start, AlignmentModes.AddSpaceBetweenItems, alignmentGroupInput), - CaptionColor = new Color (Color.Black), + CaptionColor = new (Color.Black), Width = 30, Y = Pos.Top (_btnToggleSplitterCollapse), HotKey = Key.F.WithAlt }; - _spinnerView = new SpinnerView { X = Pos.Align (Alignment.Start, AlignmentModes.AddSpaceBetweenItems, alignmentGroupInput), Y = Pos.AnchorEnd (1), Visible = false }; + + _spinnerView = new() + { X = Pos.Align (Alignment.Start, AlignmentModes.AddSpaceBetweenItems, alignmentGroupInput), Y = Pos.AnchorEnd (1), Visible = false }; _tbFind.TextChanged += (s, o) => RestartSearch (); @@ -242,7 +245,7 @@ internal FileDialog (IFileSystem fileSystem) _tableView.Style.ShowHorizontalHeaderUnderline = true; _tableView.Style.ShowHorizontalScrollIndicators = true; - _history = new FileDialogHistory (this); + _history = new (this); _tbPath.TextChanged += (s, e) => PathChanged (); @@ -398,10 +401,10 @@ protected override bool OnDrawingContent () Move (0, Viewport.Height / 2); - SetAttribute (new Attribute (Color.Red, ColorScheme.Normal.Background)); - Driver.AddStr (new string (' ', feedbackPadLeft)); + SetAttribute (new (Color.Red, ColorScheme.Normal.Background)); + Driver.AddStr (new (' ', feedbackPadLeft)); Driver.AddStr (_feedback); - Driver.AddStr (new string (' ', feedbackPadRight)); + Driver.AddStr (new (' ', feedbackPadRight)); } return true; @@ -430,9 +433,9 @@ public override void OnLoaded () _tbPath.Caption = Style.PathCaption; _tbFind.Caption = Style.SearchCaption; - _tbPath.Autocomplete.ColorScheme = new ColorScheme (_tbPath.ColorScheme) + _tbPath.Autocomplete.ColorScheme = new (_tbPath.ColorScheme) { - Normal = new Attribute (Color.Black, _tbPath.ColorScheme.Normal.Background) + Normal = new (Color.Black, _tbPath.ColorScheme.Normal.Background) }; _treeRoots = Style.TreeRootGetter (); @@ -449,18 +452,18 @@ public override void OnLoaded () // Fiddle factor int width = AllowedTypes.Max (a => a.ToString ().Length) + 6; - _allowedTypeMenu = new MenuBarItem ( - "", - _allowedTypeMenuItems = AllowedTypes.Select ( - (a, i) => new MenuItem ( - a.ToString (), - null, - () => { AllowedTypeMenuClicked (i); }) - ) - .ToArray () - ); - - _allowedTypeMenuBar = new MenuBar + _allowedTypeMenu = new ( + "", + _allowedTypeMenuItems = AllowedTypes.Select ( + (a, i) => new MenuItem ( + a.ToString (), + null, + () => { AllowedTypeMenuClicked (i); }) + ) + .ToArray () + ); + + _allowedTypeMenuBar = new() { Width = width, Y = 1, @@ -476,10 +479,10 @@ public override void OnLoaded () // TODO: Using v1's menu bar here is a hack. Need to upgrade this. _allowedTypeMenuBar.DrawingContent += (s, e) => - { - _allowedTypeMenuBar.Move (e.NewViewport.Width - 1, 0); - Driver.AddRune (Glyphs.DownArrow); - }; + { + _allowedTypeMenuBar.Move (e.NewViewport.Width - 1, 0); + Driver.AddRune (Glyphs.DownArrow); + }; Add (_allowedTypeMenuBar); } @@ -803,12 +806,12 @@ private ColorScheme ColorGetter (CellColorGetterArgs args) var black = new Color (Color.Black); // TODO: Add some kind of cache for this - return new ColorScheme + return new() { - Normal = new Attribute (color, black), - HotNormal = new Attribute (color, black), - Focus = new Attribute (black, color), - HotFocus = new Attribute (black, color) + Normal = new (color, black), + HotNormal = new (color, black), + Focus = new (black, color), + HotFocus = new (black, color) }; } @@ -895,7 +898,7 @@ private string GetProposedNewSortOrder (int clickedCol, out bool isAsc) private string GetToggleSplitterText (bool isExpanded) { return isExpanded - ? new string ((char)Glyphs.LeftArrow.Value, 2) + ? new ((char)Glyphs.LeftArrow.Value, 2) : new string ((char)Glyphs.RightArrow.Value, 2); } @@ -1225,49 +1228,48 @@ private void ShowCellContextMenu (Point? clickedCell, MouseEventArgs e) return; } - var contextMenu = new ContextMenu - { - Position = new Point (e.Position.X + 1, e.Position.Y + 1) - }; + PopoverMenu? contextMenu = new ( + [ + new (Strings.fdCtxNew, string.Empty, New), + new (Strings.fdCtxRename, string.Empty, Rename), + new (Strings.fdCtxDelete, string.Empty, Delete) + ]); - var menuItems = new MenuBarItem ( - [ - new MenuItem (Strings.fdCtxNew, string.Empty, New), - new MenuItem (Strings.fdCtxRename, string.Empty, Rename), - new MenuItem (Strings.fdCtxDelete, string.Empty, Delete) - ] - ); _tableView.SetSelection (clickedCell.Value.X, clickedCell.Value.Y, false); - contextMenu.Show (menuItems); + // Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused + // and the context menu is disposed when it is closed. + Application.Popover?.Register (contextMenu); + + contextMenu?.MakeVisible (e.ScreenPosition); } private void ShowHeaderContextMenu (int clickedCol, MouseEventArgs e) { string sort = GetProposedNewSortOrder (clickedCol, out bool isAsc); - var contextMenu = new ContextMenu - { - Position = new Point (e.Position.X + 1, e.Position.Y + 1) - }; - - var menuItems = new MenuBarItem ( - [ - new MenuItem ( - string.Format ( - Strings.fdCtxHide, - StripArrows (_tableView.Table.ColumnNames [clickedCol]) - ), - string.Empty, - () => HideColumn (clickedCol) - ), - new MenuItem ( - StripArrows (sort), - string.Empty, - () => SortColumn (clickedCol, isAsc)) - ] - ); - contextMenu.Show (menuItems); + PopoverMenu? contextMenu = new ( + [ + new ( + string.Format ( + Strings.fdCtxHide, + StripArrows (_tableView.Table.ColumnNames [clickedCol]) + ), + string.Empty, + () => HideColumn (clickedCol) + ), + new ( + StripArrows (sort), + string.Empty, + () => SortColumn (clickedCol, isAsc)) + ] + ); + + // Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused + // and the context menu is disposed when it is closed. + Application.Popover?.Register (contextMenu); + + contextMenu?.MakeVisible (e.ScreenPosition); } private void SortColumn (int clickedCol) @@ -1618,6 +1620,7 @@ bool IDesignable.EnableForDesign () { Modal = false; OnLoaded (); + return true; } } diff --git a/Terminal.Gui/Views/Menu/ContextMenu.cs b/Terminal.Gui/Views/Menu/ContextMenu.cs deleted file mode 100644 index 90f06c6752..0000000000 --- a/Terminal.Gui/Views/Menu/ContextMenu.cs +++ /dev/null @@ -1,283 +0,0 @@ -#nullable enable - -namespace Terminal.Gui; - -/// -/// ContextMenu provides a pop-up menu that can be positioned anywhere within a . ContextMenu is -/// analogous to and, once activated, works like a sub-menu of a (but -/// can be positioned anywhere). -/// -/// By default, a ContextMenu with sub-menus is displayed in a cascading manner, where each sub-menu pops out of -/// the ContextMenu frame (either to the right or left, depending on where the ContextMenu is relative to the edge -/// of the screen). By setting to , this behavior can be -/// changed such that all sub-menus are drawn within the ContextMenu frame. -/// -/// -/// ContextMenus can be activated using the Shift-F10 key (by default; use the to change to -/// another key). -/// -/// -/// Callers can cause the ContextMenu to be activated on a right-mouse click (or other interaction) by calling -/// . -/// -/// ContextMenus are located using screen coordinates and appear above all other Views. -/// -public sealed class ContextMenu : IDisposable -{ - private static MenuBar? _menuBar; - - private Toplevel? _container; - private Key _key = DefaultKey; - private MouseFlags _mouseFlags = MouseFlags.Button3Clicked; - - /// Initializes a context menu with no menu items. - public ContextMenu () - { - if (IsShow) - { - Hide (); - IsShow = false; - } - } - - /// The default shortcut key for activating the context menu. - [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] - public static Key DefaultKey { get; set; } = Key.F10.WithShift; - - /// - /// Sets or gets whether the context menu be forced to the right, ensuring it is not clipped, if the x position is - /// less than zero. The default is which means the context menu will be forced to the right. If - /// set to , the context menu will be clipped on the left if x is less than zero. - /// - public bool ForceMinimumPosToZero { get; set; } = true; - - /// The host which position will be used, otherwise if it's null the container will be used. - public View? Host { get; set; } - - /// Gets whether the ContextMenu is showing or not. - public static bool IsShow { get; private set; } - - /// Specifies the key that will activate the context menu. - public Key Key - { - get => _key; - set - { - Key oldKey = _key; - _key = value; - KeyChanged?.Invoke (this, new KeyChangedEventArgs (oldKey, _key)); - } - } - - /// Gets the that is hosting this context menu. - public MenuBar? MenuBar => _menuBar; - - /// Gets or sets the menu items for this context menu. - public MenuBarItem? MenuItems { get; private set; } - - /// specifies the mouse action used to activate the context menu by mouse. - public MouseFlags MouseFlags - { - get => _mouseFlags; - set - { - MouseFlags oldFlags = _mouseFlags; - _mouseFlags = value; - MouseFlagsChanged?.Invoke (this, new MouseFlagsChangedEventArgs (oldFlags, value)); - } - } - - /// Gets or sets the menu position. - public Point Position { get; set; } - - /// - /// Gets or sets if sub-menus will be displayed using a "single frame" menu style. If , the - /// ContextMenu and any sub-menus that would normally cascade will be displayed within a single frame. If - /// (the default), sub-menus will cascade using separate frames for each level of the menu - /// hierarchy. - /// - public bool UseSubMenusSingleFrame { get; set; } - - /// Disposes the context menu object. - public void Dispose () - { - if (_menuBar is { }) - { - _menuBar.MenuAllClosed -= MenuBar_MenuAllClosed; - _container?.Remove (_menuBar); - } - Application.UngrabMouse (); - _menuBar?.Dispose (); - _menuBar = null; - IsShow = false; - - if (_container is { }) - { - _container.Closing -= Container_Closing; - _container.Deactivate -= Container_Deactivate; - _container.Disposing -= Container_Disposing; - } - } - - /// Hides (closes) the ContextMenu. - public void Hide () - { - RemoveKeyBindings (MenuItems); - _menuBar?.CleanUp (); - IsShow = false; - } - - private void RemoveKeyBindings (MenuBarItem? menuBarItem) - { - if (menuBarItem is null) - { - return; - } - - foreach (MenuItem? menuItem in menuBarItem.Children!) - { - // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract - if (menuItem is null) - { - continue; - } - - if (menuItem is MenuBarItem barItem) - { - RemoveKeyBindings (barItem); - } - else - { - if (menuItem.ShortcutKey != Key.Empty) - { - // Remove an existent ShortcutKey - _menuBar?.HotKeyBindings.Remove (menuItem.ShortcutKey!); - } - } - } - } - - /// Event invoked when the is changed. - public event EventHandler? KeyChanged; - - /// Event invoked when the is changed. - public event EventHandler? MouseFlagsChanged; - - /// Shows (opens) the ContextMenu, displaying the s it contains. - public void Show (MenuBarItem? menuItems) - { - if (_menuBar is { }) - { - Hide (); - Dispose (); - } - - if (menuItems is null || menuItems.Children!.Length == 0) - { - return; - } - - MenuItems = menuItems; - _container = GetTopSuperView (Host); - _container!.Closing += Container_Closing; - _container.Deactivate += Container_Deactivate; - _container.Disposing += Container_Disposing; - Rectangle viewport = _container.Viewport; - Point position = Position; - - if (Host is { }) - { - Point pos = Host.Frame.Location; - pos.Y += Host.Frame.Height > 0 ? Host.Frame.Height - 1 : 0; - - if (position != pos) - { - Position = position = pos; - } - } - - Rectangle rect = Menu.MakeFrame (position.X, position.Y, MenuItems.Children); - - if (rect.Right >= viewport.Right) - { - if (viewport.Right - rect.Width >= 0 || !ForceMinimumPosToZero) - { - position.X = viewport.Right - rect.Width; - } - else if (ForceMinimumPosToZero) - { - position.X = 0; - } - } - else if (ForceMinimumPosToZero && position.X < 0) - { - position.X = 0; - } - - if (rect.Bottom >= viewport.Bottom) - { - if (viewport.Bottom - rect.Height - 1 >= 0 || !ForceMinimumPosToZero) - { - if (Host is null) - { - position.Y = viewport.Bottom - rect.Height - 1; - } - else - { - Point pos = Host.Frame.Location; - position.Y = pos.Y - rect.Height - 1; - } - } - else if (ForceMinimumPosToZero) - { - position.Y = 0; - } - } - else if (ForceMinimumPosToZero && position.Y < 0) - { - position.Y = 0; - } - - _menuBar = new MenuBar - { - X = position.X, - Y = position.Y, - Width = 0, - Height = 0, - UseSubMenusSingleFrame = UseSubMenusSingleFrame, - Key = Key, - Menus = [MenuItems] - }; - - _menuBar._isContextMenuLoading = true; - _menuBar.MenuAllClosed += MenuBar_MenuAllClosed; - - _container.Add (_menuBar); - IsShow = true; - _menuBar.OpenMenu (); - } - - internal static Toplevel? GetTopSuperView (View? view) - { - if (view is Toplevel toplevel) - { - return toplevel; - } - - for (View? sv = view?.SuperView; sv != null; sv = sv.SuperView) - { - if (sv is Toplevel top) - { - return top; - } - } - - return (Toplevel?)view?.SuperView ?? Application.Top; - } - - private void Container_Closing (object? sender, ToplevelClosingEventArgs obj) { Hide (); } - private void Container_Deactivate (object? sender, ToplevelEventArgs e) { Hide (); } - private void Container_Disposing (object? sender, EventArgs e) { Dispose (); } - - private void MenuBar_MenuAllClosed (object? sender, EventArgs e) { Hide (); } -} diff --git a/Terminal.Gui/Views/Menu/ContextMenuv2.cs b/Terminal.Gui/Views/Menu/ContextMenuv2.cs deleted file mode 100644 index 994aec4a01..0000000000 --- a/Terminal.Gui/Views/Menu/ContextMenuv2.cs +++ /dev/null @@ -1,104 +0,0 @@ -#nullable enable - -using System.Diagnostics; - -namespace Terminal.Gui; - -/// -/// ContextMenuv2 provides a Popover menu that can be positioned anywhere within a . -/// -/// To show the ContextMenu, set to the ContextMenu object and set -/// property to . -/// -/// -/// The menu will be hidden when the user clicks outside the menu or when the user presses . -/// -/// -/// To explicitly hide the menu, set property to . -/// -/// -/// is the key used to activate the ContextMenus (Shift+F10 by default). Callers can use this in -/// their keyboard handling code. -/// -/// The menu will be displayed at the current mouse coordinates. -/// -public class ContextMenuv2 : PopoverMenu, IDesignable -{ - - /// - /// The mouse flags that will trigger the context menu. The default is which is typically the right mouse button. - /// - public MouseFlags MouseFlags { get; set; } = MouseFlags.Button3Clicked; - - /// Initializes a context menu with no menu items. - public ContextMenuv2 () : this ([]) { } - - /// - public ContextMenuv2 (Menuv2? menu) : base (menu) - { - Key = DefaultKey; - } - - /// - public ContextMenuv2 (IEnumerable? menuItems) : this (new Menuv2 (menuItems)) - { - } - - private Key _key = DefaultKey; - - /// Specifies the key that will activate the context menu. - public Key Key - { - get => _key; - set - { - Key oldKey = _key; - _key = value; - KeyChanged?.Invoke (this, new KeyChangedEventArgs (oldKey, _key)); - } - } - - /// Event raised when the is changed. - public event EventHandler? KeyChanged; - - /// - public bool EnableForDesign () - { - var shortcut = new Shortcut - { - Text = "Quit", - Title = "Q_uit", - Key = Key.Z.WithCtrl, - }; - - Add (shortcut); - - shortcut = new Shortcut - { - Text = "Help Text", - Title = "Help", - Key = Key.F1, - }; - - Add (shortcut); - - shortcut = new Shortcut - { - Text = "Czech", - CommandView = new CheckBox () - { - Title = "_Check" - }, - Key = Key.F9, - CanFocus = false - }; - - Add (shortcut); - - // HACK: This enables All Views Tester to show the CM if DefaultKey is pressed - AddCommand (Command.Context, () => Visible = true); - HotKeyBindings.Add (DefaultKey, Command.Context); - - return true; - } -} diff --git a/Terminal.Gui/Views/Menu/MenuBarItemv2.cs b/Terminal.Gui/Views/Menu/MenuBarItemv2.cs index 6fe9f121eb..0ed8cdfcd2 100644 --- a/Terminal.Gui/Views/Menu/MenuBarItemv2.cs +++ b/Terminal.Gui/Views/Menu/MenuBarItemv2.cs @@ -74,7 +74,7 @@ public MenuBarItemv2 (string commandText, IEnumerable menuItems) null, Command.NotBound, commandText, - new (new (menuItems))) + new (menuItems)) { } // TODO: Hide base.SubMenu? diff --git a/Terminal.Gui/Views/Menu/MenuBarv2.cs b/Terminal.Gui/Views/Menu/MenuBarv2.cs index a129321d5d..6ad4560eb1 100644 --- a/Terminal.Gui/Views/Menu/MenuBarv2.cs +++ b/Terminal.Gui/Views/Menu/MenuBarv2.cs @@ -55,7 +55,7 @@ public override void EndInit () Border.LineStyle = LineStyle.None; } - // TODO: This needs to be done whenever a menuitem in any memubaritem changes + // TODO: This needs to be done whenever a menuitem in any MenuBarItem changes foreach (MenuBarItemv2? mbi in SubViews.Select(s => s as MenuBarItemv2)) { Application.Popover?.Register (mbi?.PopoverMenu); @@ -86,7 +86,7 @@ private void ShowPopover (MenuBarItemv2? menuBarItem) && Application.Popover?.GetActivePopover () is PopoverMenu popoverMenu && popoverMenu?.Root?.SuperMenuItem?.SuperView == this) { - Application.Popover?.HidePopover (popoverMenu); + Application.Popover?.Hide (popoverMenu); } menuBarItem?.PopoverMenu?.MakeVisible (new Point (menuBarItem.FrameToScreen ().X, menuBarItem.FrameToScreen ().Bottom)); @@ -167,176 +167,6 @@ public bool EnableForDesign (ref readonly TContext context) where TCon ] ) ); - - // if (context is not Func actionFn) - // { - // actionFn = (_) => true; - // } - - // View? targetView = context as View; - - // Add (new MenuItemv2 (targetView, - // Command.NotBound, - // "_File", - // new MenuItem [] - // { - // new ( - // "_New", - // "", - // () => actionFn ("New"), - // null, - // null, - // KeyCode.CtrlMask | KeyCode.N - // ), - // new ( - // "_Open", - // "", - // () => actionFn ("Open"), - // null, - // null, - // KeyCode.CtrlMask | KeyCode.O - // ), - // new ( - // "_Save", - // "", - // () => actionFn ("Save"), - // null, - // null, - // KeyCode.CtrlMask | KeyCode.S - // ), - //#pragma warning disable CS8625 // Cannot convert null literal to non-nullable reference type. - // null, - //#pragma warning restore CS8625 // Cannot convert null literal to non-nullable reference type. - - // // Don't use Application.Quit so we can disambiguate between quitting and closing the toplevel - // new ( - // "_Quit", - // "", - // () => actionFn ("Quit"), - // null, - // null, - // KeyCode.CtrlMask | KeyCode.Q - // ) - // } - // ), - // new MenuBarItem ( - // "_Edit", - // new MenuItem [] - // { - // new ( - // "_Copy", - // "", - // () => actionFn ("Copy"), - // null, - // null, - // KeyCode.CtrlMask | KeyCode.C - // ), - // new ( - // "C_ut", - // "", - // () => actionFn ("Cut"), - // null, - // null, - // KeyCode.CtrlMask | KeyCode.X - // ), - // new ( - // "_Paste", - // "", - // () => actionFn ("Paste"), - // null, - // null, - // KeyCode.CtrlMask | KeyCode.V - // ), - // new MenuBarItem ( - // "_Find and Replace", - // new MenuItem [] - // { - // new ( - // "F_ind", - // "", - // () => actionFn ("Find"), - // null, - // null, - // KeyCode.CtrlMask | KeyCode.F - // ), - // new ( - // "_Replace", - // "", - // () => actionFn ("Replace"), - // null, - // null, - // KeyCode.CtrlMask | KeyCode.H - // ), - // new MenuBarItem ( - // "_3rd Level", - // new MenuItem [] - // { - // new ( - // "_1st", - // "", - // () => actionFn ( - // "1" - // ), - // null, - // null, - // KeyCode.F1 - // ), - // new ( - // "_2nd", - // "", - // () => actionFn ( - // "2" - // ), - // null, - // null, - // KeyCode.F2 - // ) - // } - // ), - // new MenuBarItem ( - // "_4th Level", - // new MenuItem [] - // { - // new ( - // "_5th", - // "", - // () => actionFn ( - // "5" - // ), - // null, - // null, - // KeyCode.CtrlMask - // | KeyCode.D5 - // ), - // new ( - // "_6th", - // "", - // () => actionFn ( - // "6" - // ), - // null, - // null, - // KeyCode.CtrlMask - // | KeyCode.D6 - // ) - // } - // ) - // } - // ), - // new ( - // "_Select All", - // "", - // () => actionFn ("Select All"), - // null, - // null, - // KeyCode.CtrlMask - // | KeyCode.ShiftMask - // | KeyCode.S - // ) - // } - // ), - // new MenuBarItem ("_About", "Top-Level", () => actionFn ("About")) - // ]; return true; } } diff --git a/Terminal.Gui/Views/Menu/MenuItemv2.cs b/Terminal.Gui/Views/Menu/MenuItemv2.cs index acf0311272..f655d0b867 100644 --- a/Terminal.Gui/Views/Menu/MenuItemv2.cs +++ b/Terminal.Gui/Views/Menu/MenuItemv2.cs @@ -6,7 +6,7 @@ namespace Terminal.Gui; /// -/// A -dervied object to be used as a menu item in a . Has title, an +/// A -derived object to be used as a menu item in a . Has title, an /// associated help text, and an action to execute on activation. /// public class MenuItemv2 : Shortcut @@ -45,7 +45,18 @@ public MenuItemv2 (View? targetView, Command command, string? commandText = null { TargetView = targetView; Command = command; + SubMenu = subMenu; + } + + /// + public MenuItemv2 (string? commandText = null, string? helpText = null, Action? action = null, Key? key = null) + : base (key ?? Key.Empty, commandText, action, helpText) + { } + /// + public MenuItemv2 (string? commandText = null, string? helpText = null, Menuv2? subMenu = null) + : base (Key.Empty, commandText, null, helpText) + { SubMenu = subMenu; } @@ -108,7 +119,7 @@ public Command Command ret = base.DispatchCommand (commandContext); } - Logging.Trace ($"{commandContext?.Source?.Title}"); + //Logging.Trace ($"{commandContext?.Source?.Title}"); RaiseAccepted (commandContext); @@ -150,7 +161,7 @@ protected override bool OnMouseEnter (CancelEventArgs eventArgs) // TODO: Consider moving Accepted to Shortcut? /// - /// Riases the / event indicating this item (or submenu) + /// Raises the / event indicating this item (or submenu) /// was accepted. This is used to determine when to hide the menu. /// /// @@ -167,7 +178,7 @@ protected override bool OnMouseEnter (CancelEventArgs eventArgs) } /// - /// Called when the user has accepted an item in this menu (or submenu. This is used to determine when to hide the + /// Called when the user has accepted an item in this menu (or submenu). This is used to determine when to hide the /// menu. /// /// @@ -176,7 +187,7 @@ protected override bool OnMouseEnter (CancelEventArgs eventArgs) protected virtual void OnAccepted (CommandEventArgs args) { } /// - /// Raised when the user has accepted an item in this menu (or submenu. This is used to determine when to hide the + /// Raised when the user has accepted an item in this menu (or submenu). This is used to determine when to hide the /// menu. /// /// diff --git a/Terminal.Gui/Views/Menu/Menuv2.cs b/Terminal.Gui/Views/Menu/Menuv2.cs index 88df61b673..269acf4e44 100644 --- a/Terminal.Gui/Views/Menu/Menuv2.cs +++ b/Terminal.Gui/Views/Menu/Menuv2.cs @@ -2,13 +2,16 @@ namespace Terminal.Gui; /// -/// A -derived object to be used as a verticaly-oriented menu. Each subview is a . +/// A -derived object to be used as a vertically-oriented menu. Each subview is a . /// public class Menuv2 : Bar { /// public Menuv2 () : this ([]) { } + /// + public Menuv2 (IEnumerable? shortcuts) : this (shortcuts?.Cast()) { } + /// public Menuv2 (IEnumerable? shortcuts) : base (shortcuts) { @@ -18,7 +21,6 @@ public Menuv2 (IEnumerable? shortcuts) : base (shortcuts) Border!.Thickness = new Thickness (1, 1, 1, 1); Border.LineStyle = LineStyle.Single; - } /// @@ -50,52 +52,44 @@ protected override void OnSubViewAdded (View view) { base.OnSubViewAdded (view); - if (view is MenuItemv2 menuItem) + switch (view) { - menuItem.CanFocus = true; - - AddCommand (menuItem.Command, RaiseAccepted); + case MenuItemv2 menuItem: + { + menuItem.CanFocus = true; - menuItem.Selecting += MenuItemOnSelecting; - menuItem.Accepting += MenuItemOnAccepting; - menuItem.Accepted += MenuItemOnAccepted; + AddCommand (menuItem.Command, RaiseAccepted); - void MenuItemOnSelecting (object? sender, CommandEventArgs e) - { - Logging.Trace ($"Selecting: {e.Context?.Source?.Title}"); - } + menuItem.Accepted += MenuItemOnAccepted; - void MenuItemOnAccepting (object? sender, CommandEventArgs e) - { - Logging.Trace ($"Accepting: {e.Context?.Source?.Title}"); - } + break; - void MenuItemOnAccepted (object? sender, CommandEventArgs e) - { - Logging.Trace ($"Accepted: {e.Context?.Source?.Title}"); - RaiseAccepted (e.Context); + void MenuItemOnAccepted (object? sender, CommandEventArgs e) + { + //Logging.Trace ($"Accepted: {e.Context?.Source?.Title}"); + RaiseAccepted (e.Context); + } } - } + case Line line: + // Grow line so we get auto-join line + line.X = Pos.Func (() => -Border!.Thickness.Left); + line.Width = Dim.Fill ()! + Dim.Func (() => Border!.Thickness.Right); - if (view is Line line) - { - // Grow line so we get autojoin line - line.X = Pos.Func (() => -Border!.Thickness.Left); - line.Width = Dim.Fill ()! + Dim.Func (() => Border!.Thickness.Right); + break; } } // TODO: Consider moving Accepted to Bar? /// - /// Riases the / event indicating an item in this menu (or submenu) + /// Raises the / event indicating an item in this menu (or submenu) /// was accepted. This is used to determine when to hide the menu. /// /// /// protected bool? RaiseAccepted (ICommandContext? ctx) { - Logging.Trace ($"RaiseAccepted: {ctx}"); + //Logging.Trace ($"RaiseAccepted: {ctx}"); CommandEventArgs args = new () { Context = ctx }; OnAccepted (args); @@ -105,7 +99,7 @@ void MenuItemOnAccepted (object? sender, CommandEventArgs e) } /// - /// Called when the user has accepted an item in this menu (or submenu. This is used to determine when to hide the menu. + /// Called when the user has accepted an item in this menu (or submenu). This is used to determine when to hide the menu. /// /// /// @@ -113,7 +107,7 @@ void MenuItemOnAccepted (object? sender, CommandEventArgs e) protected virtual void OnAccepted (CommandEventArgs args) { } /// - /// Raised when the user has accepted an item in this menu (or submenu. This is used to determine when to hide the menu. + /// Raised when the user has accepted an item in this menu (or submenu). This is used to determine when to hide the menu. /// /// /// @@ -157,7 +151,7 @@ internal void RaiseSelectedMenuItemChanged (MenuItemv2? selected) } /// - /// Called when the the selected menu item has changed. + /// Called when the selected menu item has changed. /// /// protected virtual void OnSelectedMenuItemChanged (MenuItemv2? selected) diff --git a/Terminal.Gui/Views/Menu/PopoverMenu.cs b/Terminal.Gui/Views/Menu/PopoverMenu.cs index 804c1a9a37..555dd9175b 100644 --- a/Terminal.Gui/Views/Menu/PopoverMenu.cs +++ b/Terminal.Gui/Views/Menu/PopoverMenu.cs @@ -1,24 +1,38 @@ #nullable enable -using System.Diagnostics; - namespace Terminal.Gui; /// -/// Provides a cascading popover menu. +/// Provides a cascading menu that pops over all other content. Can be used as a context menu or a drop-down +/// menu as part of . /// -public class PopoverMenu : PopoverBaseImpl +/// +/// +/// To use as a context menu, register the popover menu with and call +/// . +/// +/// +public class PopoverMenu : PopoverBaseImpl, IDesignable { /// /// Initializes a new instance of the class. /// - public PopoverMenu () : this (null) { } + public PopoverMenu () : this ((Menuv2?)null) { } + + /// + public PopoverMenu (IEnumerable? menuItems) : this (new Menuv2 (menuItems)) { } + + /// + public PopoverMenu (IEnumerable? menuItems) : this (new Menuv2 (menuItems)) { } /// /// Initializes a new instance of the class with the specified root . /// public PopoverMenu (Menuv2? root) { + Key = DefaultKey; + base.Visible = false; + //base.ColorScheme = Colors.ColorSchemes ["Menu"]; Root = root; @@ -77,11 +91,6 @@ public PopoverMenu (Menuv2? root) bool? MoveRight (ICommandContext? ctx) { - if (Focused == Root) - { - return false; - } - if (MostFocused is MenuItemv2 { SubMenu.Visible: true } focused) { focused.SubMenu.SetFocus (); @@ -93,18 +102,36 @@ public PopoverMenu (Menuv2? root) } } - /// - /// The mouse flags that will cause the popover menu to be visible. The default is - /// which is typically the right mouse button. - /// - public static MouseFlags MouseFlags { get; set; } = MouseFlags.Button3Clicked; + private Key _key = DefaultKey; + + /// Specifies the key that will activate the context menu. + public Key Key + { + get => _key; + set + { + Key oldKey = _key; + _key = value; + KeyChanged?.Invoke (this, new (oldKey, _key)); + } + } + + /// Raised when is changed. + public event EventHandler? KeyChanged; /// The default key for activating popover menus. [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] public static Key DefaultKey { get; set; } = Key.F10.WithShift; /// - /// Makes the popover menu visible and locates it at . The actual position of the menu + /// The mouse flags that will cause the popover menu to be visible. The default is + /// which is typically the right mouse button. + /// + public MouseFlags MouseFlags { get; set; } = MouseFlags.Button3Clicked; + + /// + /// Makes the popover menu visible and locates it at . The actual position of the + /// menu /// will be adjusted to /// ensure the menu fully fits on the screen, and the mouse cursor is over the first cell of the /// first MenuItem. @@ -114,11 +141,12 @@ public void MakeVisible (Point? idealScreenPosition = null) { UpdateKeyBindings (); SetPosition (idealScreenPosition); - Application.Popover?.ShowPopover (this); + Application.Popover?.Show (this); } /// - /// Locates the popover menu at . The actual position of the menu will be adjusted to + /// Locates the popover menu at . The actual position of the menu will be + /// adjusted to /// ensure the menu fully fits on the screen, and the mouse cursor is over the first cell of the /// first MenuItem (if possible). /// @@ -127,21 +155,24 @@ public void SetPosition (Point? idealScreenPosition = null) { idealScreenPosition ??= Application.GetLastMousePosition (); - if (idealScreenPosition is { } && Root is { }) + if (idealScreenPosition is null || Root is null) { - Point pos = idealScreenPosition.Value; + return; + } - if (!Root.IsInitialized) - { - Root.BeginInit(); - Root.EndInit (); - Root.Layout (); - } - pos = GetMostVisibleLocationForSubMenu (Root, pos); + Point pos = idealScreenPosition.Value; - Root.X = pos.X; - Root.Y = pos.Y; + if (!Root.IsInitialized) + { + Root.BeginInit (); + Root.EndInit (); + Root.Layout (); } + + pos = GetMostVisibleLocationForSubMenu (Root, pos); + + Root.X = pos.X; + Root.Y = pos.Y; } /// @@ -156,7 +187,7 @@ protected override void OnVisibleChanged () else { HideAndRemoveSubMenu (_root); - Application.Popover?.HidePopover (this); + Application.Popover?.Hide (this); } } @@ -208,48 +239,34 @@ private void UpdateKeyBindings () // TODO: And it needs to clear them first IEnumerable all = GetMenuItemsOfAllSubMenus (); - foreach (MenuItemv2 menuItem in all.Where(mi => mi.Command != Command.NotBound)) + foreach (MenuItemv2 menuItem in all.Where (mi => mi.Command != Command.NotBound)) { + Key? key; if (menuItem.TargetView is { }) { // A TargetView implies HotKey - // Automatically set MenuItem.Key - Key? key = menuItem.TargetView.HotKeyBindings.GetFirstFromCommands (menuItem.Command); - - if (key is { IsValid: true }) - { - if (menuItem.Key.IsValid) - { - //Logging.Warning ("Do not specify a Key for MenuItems where a Command is specified. Key will be determined automatically."); - } - - menuItem.Key = key; - Logging.Trace ($"HotKey: {menuItem.Key}->{menuItem.Command}"); - } + key = menuItem.TargetView.HotKeyBindings.GetFirstFromCommands (menuItem.Command); } else { // No TargetView implies Application HotKey - Key? key = Application.KeyBindings.GetFirstFromCommands (menuItem.Command); + key = Application.KeyBindings.GetFirstFromCommands (menuItem.Command); + } - if (key is { IsValid: true }) - { - if (menuItem.Key.IsValid) - { - // Logging.Warning ("App HotKey: Do not specify a Key for MenuItems where a Command is specified. Key will be determined automatically."); - } + if (key is not { IsValid: true }) + { + continue; + } - menuItem.Key = key; - Logging.Trace ($"App HotKey: {menuItem.Key}->{menuItem.Command}"); - } + if (menuItem.Key.IsValid) + { + //Logging.Warning ("Do not specify a Key for MenuItems where a Command is specified. Key will be determined automatically."); } - } - foreach (MenuItemv2 menuItem in all.Where (mi => mi is { Command: Command.NotBound, Key.IsValid: true })) - { + menuItem.Key = key; + //Logging.Trace ($"HotKey: {menuItem.Key}->{menuItem.Command}"); } - } /// @@ -332,14 +349,9 @@ internal void ShowSubMenu (MenuItemv2? menuItem) { var menu = menuItem?.SuperView as Menuv2; - if (menu is { }) - { - menu.Layout (); - } - // If there's a visible peer, remove / hide it - - // Debug.Assert (menu is null || menu?.SubViews.Count (v => v is MenuItemv2 { SubMenu.Visible: true }) < 2); + menu?.Layout (); + // If there's a visible peer, remove / hide it if (menu?.SubViews.FirstOrDefault (v => v is MenuItemv2 { SubMenu.Visible: true }) is MenuItemv2 visiblePeer) { HideAndRemoveSubMenu (visiblePeer.SubMenu); @@ -391,7 +403,7 @@ private void AddAndShowSubMenu (Menuv2? menu) // TODO: Find the menu item below the mouse, if any, and select it // TODO: Enable No Border menu style - menu.Border.LineStyle = LineStyle.Single; + menu.Border!.LineStyle = LineStyle.Single; menu.Border.Thickness = new (1); if (!menu.IsInitialized) @@ -403,7 +415,6 @@ private void AddAndShowSubMenu (Menuv2? menu) menu.ClearFocus (); base.Add (menu); - // IMPORTANT: This must be done after adding the menu to the super view or Add will try // to set focus to it. menu.Visible = true; @@ -417,8 +428,6 @@ private void HideAndRemoveSubMenu (Menuv2? menu) if (menu is { Visible: true }) { // If there's a visible submenu, remove / hide it - // Debug.Assert (menu.SubViews.Count (v => v is MenuItemv2 { SubMenu.Visible: true }) <= 1); - if (menu.SubViews.FirstOrDefault (v => v is MenuItemv2 { SubMenu.Visible: true }) is MenuItemv2 visiblePeer) { HideAndRemoveSubMenu (visiblePeer.SubMenu); @@ -448,12 +457,12 @@ private void MenuOnAccepting (object? sender, CommandEventArgs e) e.Cancel = true; } - Logging.Trace ($"{e.Context?.Source?.Title}"); + //Logging.Trace ($"{e.Context?.Source?.Title}"); } private void MenuAccepted (object? sender, CommandEventArgs e) { - Logging.Trace ($"{e.Context?.Source?.Title}"); + //Logging.Trace ($"{e.Context?.Source?.Title}"); if (e.Context?.Source is MenuItemv2 { SubMenu: null }) { @@ -467,14 +476,14 @@ private void MenuAccepted (object? sender, CommandEventArgs e) } /// - /// Riases the / event indicating a menu (or submenu) + /// Raises the / event indicating a menu (or submenu) /// was accepted and the Menus in the PopoverMenu were hidden. Use this to determine when to hide the PopoverMenu. /// /// /// protected bool? RaiseAccepted (ICommandContext? ctx) { - Logging.Trace ($"RaiseAccepted: {ctx}"); + //Logging.Trace ($"RaiseAccepted: {ctx}"); CommandEventArgs args = new () { Context = ctx }; OnAccepted (args); @@ -529,4 +538,21 @@ protected override void Dispose (bool disposing) base.Dispose (disposing); } + + + /// + public bool EnableForDesign (ref readonly TContext context) where TContext : notnull + { + Root = new Menuv2 ( + [ + new MenuItemv2 (this, Command.Cut), + new MenuItemv2 (this, Command.Copy), + new MenuItemv2 (this, Command.Paste), + new Line (), + new MenuItemv2 (this, Command.SelectAll) + ]); + + Visible = true; + return true; + } } diff --git a/Terminal.Gui/Views/Menu/Menu.cs b/Terminal.Gui/Views/Menuv1/Menu.cs similarity index 99% rename from Terminal.Gui/Views/Menu/Menu.cs rename to Terminal.Gui/Views/Menuv1/Menu.cs index b7d14d3f0b..a4d2411402 100644 --- a/Terminal.Gui/Views/Menu/Menu.cs +++ b/Terminal.Gui/Views/Menuv1/Menu.cs @@ -3,8 +3,7 @@ namespace Terminal.Gui; /// -/// An internal class used to represent a menu pop-up menu. Created and managed by and -/// . +/// An internal class used to represent a menu pop-up menu. Created and managed by . /// internal sealed class Menu : View { diff --git a/Terminal.Gui/Views/Menu/MenuBar.cs b/Terminal.Gui/Views/Menuv1/MenuBar.cs similarity index 99% rename from Terminal.Gui/Views/Menu/MenuBar.cs rename to Terminal.Gui/Views/Menuv1/MenuBar.cs index 38d5dc778c..bfde40c1be 100644 --- a/Terminal.Gui/Views/Menu/MenuBar.cs +++ b/Terminal.Gui/Views/Menuv1/MenuBar.cs @@ -17,7 +17,6 @@ namespace Terminal.Gui; /// The appears on the first row of the SuperView and uses the full /// width. /// -/// See also: /// The provides global hot keys for the application. See . /// /// When the menu is created key bindings for each menu item and its sub-menu items are added for each menu diff --git a/Terminal.Gui/Views/Menu/MenuBarItem.cs b/Terminal.Gui/Views/Menuv1/MenuBarItem.cs similarity index 100% rename from Terminal.Gui/Views/Menu/MenuBarItem.cs rename to Terminal.Gui/Views/Menuv1/MenuBarItem.cs diff --git a/Terminal.Gui/Views/Menu/MenuClosingEventArgs.cs b/Terminal.Gui/Views/Menuv1/MenuClosingEventArgs.cs similarity index 100% rename from Terminal.Gui/Views/Menu/MenuClosingEventArgs.cs rename to Terminal.Gui/Views/Menuv1/MenuClosingEventArgs.cs diff --git a/Terminal.Gui/Views/Menu/MenuItem.cs b/Terminal.Gui/Views/Menuv1/MenuItem.cs similarity index 99% rename from Terminal.Gui/Views/Menu/MenuItem.cs rename to Terminal.Gui/Views/Menuv1/MenuItem.cs index 7a222ebca1..d5dd714bc1 100644 --- a/Terminal.Gui/Views/Menu/MenuItem.cs +++ b/Terminal.Gui/Views/Menuv1/MenuItem.cs @@ -255,7 +255,7 @@ private void GetHotKey () /// /// Shortcut defines a key binding to the MenuItem that will invoke the MenuItem's action globally for the - /// that is the parent of the or this + /// that is the parent of the this /// . /// /// The will be drawn on the MenuItem to the right of the and diff --git a/Terminal.Gui/Views/Menu/MenuItemCheckStyle.cs b/Terminal.Gui/Views/Menuv1/MenuItemCheckStyle.cs similarity index 100% rename from Terminal.Gui/Views/Menu/MenuItemCheckStyle.cs rename to Terminal.Gui/Views/Menuv1/MenuItemCheckStyle.cs diff --git a/Terminal.Gui/Views/Menu/MenuOpenedEventArgs.cs b/Terminal.Gui/Views/Menuv1/MenuOpenedEventArgs.cs similarity index 100% rename from Terminal.Gui/Views/Menu/MenuOpenedEventArgs.cs rename to Terminal.Gui/Views/Menuv1/MenuOpenedEventArgs.cs diff --git a/Terminal.Gui/Views/Menu/MenuOpeningEventArgs.cs b/Terminal.Gui/Views/Menuv1/MenuOpeningEventArgs.cs similarity index 100% rename from Terminal.Gui/Views/Menu/MenuOpeningEventArgs.cs rename to Terminal.Gui/Views/Menuv1/MenuOpeningEventArgs.cs diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index 604e34e28d..a6e5d58c13 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -418,9 +418,9 @@ public TextField () /// Gets or sets the foreground to use when rendering . public Color CaptionColor { get; set; } - /// Get the for this view. + /// Get the Context Menu for this view. [CanBeNull] - public ContextMenuv2 ContextMenu { get; private set; } + public PopoverMenu ContextMenu { get; private set; } /// Sets or gets the current cursor position. public virtual int CursorPosition @@ -800,7 +800,7 @@ protected override bool OnMouseEvent (MouseEventArgs ev) && !ev.Flags.HasFlag (MouseFlags.ReportMousePosition) && !ev.Flags.HasFlag (MouseFlags.Button1DoubleClicked) && !ev.Flags.HasFlag (MouseFlags.Button1TripleClicked) - && !ev.Flags.HasFlag (PopoverMenu.MouseFlags)) + && !ev.Flags.HasFlag (ContextMenu!.MouseFlags)) { return false; } @@ -900,7 +900,7 @@ protected override bool OnMouseEvent (MouseEventArgs ev) ClearAllSelection (); PrepareSelection (0, _text.Count); } - else if (ev.Flags == PopoverMenu.MouseFlags) + else if (ev.Flags == ContextMenu!.MouseFlags) { PositionCursor (ev); ShowContextMenu (false); @@ -1226,7 +1226,7 @@ private void Adjust () private void CreateContextMenu () { DisposeContextMenu (); - ContextMenuv2 menu = new (new List () + PopoverMenu menu = new (new List () { new (this, Command.SelectAll, Strings.ctxSelectAll), new (this, Command.DeleteAll, Strings.ctxDeleteAll), diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index f89c6aeab8..c3c768ad61 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -2490,8 +2490,8 @@ public bool AllowsTab /// public IAutocomplete Autocomplete { get; protected set; } = new TextViewAutocomplete (); - /// Get the for this view. - public ContextMenuv2? ContextMenu { get; private set; } + /// Get the Context Menu. + public PopoverMenu? ContextMenu { get; private set; } /// Gets the cursor column. /// The cursor column. @@ -4148,17 +4148,17 @@ private void Adjust () private void AppendClipboard (string text) { Clipboard.Contents += text; } - private ContextMenuv2 CreateContextMenu () + private PopoverMenu CreateContextMenu () { - ContextMenuv2 menu = new (new List () + PopoverMenu menu = new (new List () { - new (this, Command.SelectAll, Strings.ctxSelectAll), - new (this, Command.DeleteAll, Strings.ctxDeleteAll), - new (this, Command.Copy, Strings.ctxCopy), - new (this, Command.Cut, Strings.ctxCut), - new (this, Command.Paste, Strings.ctxPaste), - new (this, Command.Undo, Strings.ctxUndo), - new (this, Command.Redo, Strings.ctxRedo), + new MenuItemv2 (this, Command.SelectAll, Strings.ctxSelectAll), + new MenuItemv2 (this, Command.DeleteAll, Strings.ctxDeleteAll), + new MenuItemv2 (this, Command.Copy, Strings.ctxCopy), + new MenuItemv2 (this, Command.Cut, Strings.ctxCut), + new MenuItemv2 (this, Command.Paste, Strings.ctxPaste), + new MenuItemv2 (this, Command.Undo, Strings.ctxUndo), + new MenuItemv2 (this, Command.Redo, Strings.ctxRedo), }); menu.KeyChanged += ContextMenu_KeyChanged; diff --git a/TerminalGuiFluentTesting/GuiTestContext.cs b/TerminalGuiFluentTesting/GuiTestContext.cs index 9a1195df81..bd3f8d974d 100644 --- a/TerminalGuiFluentTesting/GuiTestContext.cs +++ b/TerminalGuiFluentTesting/GuiTestContext.cs @@ -57,7 +57,7 @@ internal GuiTestContext (Func topLevelBuilder, int width, int height, .CreateLogger ("Test Logging"); Logging.Logger = logger; - v2.Init (null, GetDriverName()); + v2.Init (null, GetDriverName ()); booting.Release (); @@ -93,12 +93,12 @@ internal GuiTestContext (Func topLevelBuilder, int width, int height, private string GetDriverName () { return _driver switch - { - V2TestDriver.V2Win => "v2win", - V2TestDriver.V2Net => "v2net", - _ => - throw new ArgumentOutOfRangeException () - }; + { + V2TestDriver.V2Win => "v2win", + V2TestDriver.V2Net => "v2net", + _ => + throw new ArgumentOutOfRangeException () + }; } /// @@ -299,14 +299,14 @@ private GuiTestContext Click (WindowsConsole.ButtonState btn, int screenX, int s case V2TestDriver.V2Net: int netButton = btn switch - { - WindowsConsole.ButtonState.Button1Pressed => 0, - WindowsConsole.ButtonState.Button2Pressed => 1, - WindowsConsole.ButtonState.Button3Pressed => 2, - WindowsConsole.ButtonState.RightmostButtonPressed => 2, - _ => throw new ArgumentOutOfRangeException(nameof(btn)) - }; - foreach (var k in NetSequences.Click(netButton,screenX,screenY)) + { + WindowsConsole.ButtonState.Button1Pressed => 0, + WindowsConsole.ButtonState.Button2Pressed => 1, + WindowsConsole.ButtonState.Button3Pressed => 2, + WindowsConsole.ButtonState.RightmostButtonPressed => 2, + _ => throw new ArgumentOutOfRangeException (nameof (btn)) + }; + foreach (var k in NetSequences.Click (netButton, screenX, screenY)) { SendNetKey (k); } @@ -452,18 +452,20 @@ public GuiTestContext Enter () /// /// Registers a right click handler on the added view (or root view) that - /// will open the supplied . + /// will open the supplied . /// - /// - /// + /// /// - public GuiTestContext WithContextMenu (ContextMenu ctx, MenuBarItem menuItems) + public GuiTestContext WithContextMenu (PopoverMenu? contextMenu) { LastView.MouseEvent += (s, e) => { if (e.Flags.HasFlag (MouseFlags.Button3Clicked)) { - ctx.Show (menuItems); + // Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused + // and the context menu is disposed when it is closed. + Application.Popover?.Register (contextMenu); + contextMenu?.MakeVisible (e.ScreenPosition); } }; diff --git a/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs b/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs index 345d7acc19..93dff6a865 100644 --- a/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs +++ b/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs @@ -63,22 +63,16 @@ public void ContextMenu_CrashesOnRight (V2TestDriver d) { var clicked = false; - var ctx = new ContextMenu (); - - var menuItems = new MenuBarItem ( - [ - new ("_New File", string.Empty, () => { clicked = true; }) - ] - ); + MenuItemv2 [] menuItems = [new ("_New File", string.Empty, () => { clicked = true; })]; using GuiTestContext c = With.A (40, 10, d) - .WithContextMenu (ctx, menuItems) + .WithContextMenu (new PopoverMenu(menuItems)) .ScreenShot ("Before open menu", _out) // Click in main area inside border .RightClick (1, 1) .ScreenShot ("After open menu", _out) - .LeftClick (3, 3) + .LeftClick (2, 2) .Stop () .WriteOutLogs (_out); Assert.True (clicked); @@ -90,34 +84,26 @@ public void ContextMenu_OpenSubmenu (V2TestDriver d) { var clicked = false; - var ctx = new ContextMenu (); - - - - var menuItems = new MenuBarItem ( - [ - new MenuItem ("One", "", null), - new MenuItem ("Two", "", null), - new MenuItem ("Three", "", null), - new MenuBarItem ( - "Four", - [ - new MenuItem ("SubMenu1", "", null), - new MenuItem ("SubMenu2", "", ()=>clicked=true), - new MenuItem ("SubMenu3", "", null), - new MenuItem ("SubMenu4", "", null), - new MenuItem ("SubMenu5", "", null), - new MenuItem ("SubMenu6", "", null), - new MenuItem ("SubMenu7", "", null) - ] - ), - new MenuItem ("Five", "", null), - new MenuItem ("Six", "", null) - ] - ); + MenuItemv2 [] menuItems = [ + new ("One", "", null), + new ("Two", "", null), + new ("Three", "", null), + new ("Four", "", new ( + [ + new ("SubMenu1", "", null), + new ("SubMenu2", "", ()=>clicked=true), + new ("SubMenu3", "", null), + new ("SubMenu4", "", null), + new ("SubMenu5", "", null), + new ("SubMenu6", "", null), + new ("SubMenu7", "", null) + ])), + new ("Five", "", null), + new ("Six", "", null) + ]; using GuiTestContext c = With.A (40, 10,d) - .WithContextMenu (ctx, menuItems) + .WithContextMenu (new PopoverMenu (menuItems)) .ScreenShot ("Before open menu", _out) // Click in main area inside border diff --git a/Tests/UnitTests/Application/ApplicationPopoverTests.cs b/Tests/UnitTests/Application/ApplicationPopoverTests.cs index 20ca401082..b491f1eb94 100644 --- a/Tests/UnitTests/Application/ApplicationPopoverTests.cs +++ b/Tests/UnitTests/Application/ApplicationPopoverTests.cs @@ -5,7 +5,7 @@ namespace Terminal.Gui.ApplicationTests; public class ApplicationPopoverTests { [Fact] - public void Popover_ApplicationInit_Inits () + public void ApplicationInit_Initializes_PopoverManager () { // Arrange Assert.Null (Application.Popover); @@ -18,7 +18,7 @@ public void Popover_ApplicationInit_Inits () } [Fact] - public void Popover_ApplicationShutdown_CleansUp () + public void Application_Shutdown_CleansUp_PopoverManager () { // Arrange Assert.Null (Application.Popover); @@ -34,7 +34,7 @@ public void Popover_ApplicationShutdown_CleansUp () } [Fact] - public void Popover_NotCleanedUp_On_End () + public void Application_End_Does_Not_CleanedUp () { // Arrange Assert.Null (Application.Popover); @@ -56,7 +56,7 @@ public void Popover_NotCleanedUp_On_End () } [Fact] - public void Popover_Active_Hidden_On_End () + public void Application_End_Hides_Active () { // Arrange Assert.Null (Application.Popover); @@ -66,9 +66,9 @@ public void Popover_Active_Hidden_On_End () var top = new Toplevel (); RunState rs = Application.Begin (top); - IPopoverTestClass popover = new (); + PopoverTestClass popover = new (); - Application.Popover?.ShowPopover (popover); + Application.Popover?.Show (popover); Assert.True (popover.Visible); // Act @@ -83,17 +83,80 @@ public void Popover_Active_Hidden_On_End () Application.Shutdown (); } - public class IPopoverTestClass : View, IPopover + [Fact] + public void Application_Shutdown_Disposes_Registered_Popovers () + { + // Arrange + Assert.Null (Application.Popover); + Application.Init (new FakeDriver ()); + + PopoverTestClass popover = new (); + + // Act + Application.Popover?.Register (popover); + Application.Shutdown (); + + // Test + Assert.Equal(1, popover.DisposedCount); + } + + [Fact] + public void Application_Shutdown_Does_Not_Dispose_DeRegistered_Popovers () { - public List HandledKeys { get; } = new List (); + // Arrange + Assert.Null (Application.Popover); + Application.Init (new FakeDriver ()); + + PopoverTestClass popover = new (); + + Application.Popover?.Register (popover); + + // Act + Application.Popover?.DeRegister (popover); + Application.Shutdown (); + + // Test + Assert.Equal (0, popover.DisposedCount); + + popover.Dispose (); + } + + [Fact] + public void Application_Shutdown_Does_Not_Dispose_ActiveNotRegistered_Popover () + { + // Arrange + Assert.Null (Application.Popover); + Application.Init (new FakeDriver ()); + + PopoverTestClass popover = new (); + + Application.Popover?.Show (popover); + + // Act + Application.Shutdown (); + + // Test + Assert.Equal (0, popover.DisposedCount); + + popover.Dispose (); + } + + public class PopoverTestClass : View, IPopover + { + public List HandledKeys { get; } = []; public int NewCommandInvokeCount { get; private set; } - public IPopoverTestClass () + // NOTE: Hides the base DisposedCount property + public new int DisposedCount { get; private set; } + + public PopoverTestClass () { CanFocus = true; AddCommand (Command.New, NewCommandHandler); HotKeyBindings.Add (Key.N.WithCtrl, Command.New); + return; + bool? NewCommandHandler (ICommandContext ctx) { NewCommandInvokeCount++; @@ -107,338 +170,13 @@ protected override bool OnKeyDown (Key key) HandledKeys.Add (key); return false; } + + /// + protected override void Dispose (bool disposing) + { + base.Dispose (disposing); + DisposedCount++; + } } - //[Fact] - //public void Popover_SetToNull () - //{ - // // Arrange - // var popover = new View (); - // Application.Popover = popover; - - // // Act - // Application.Popover = null; - - // // Assert - // Assert.Null (Application.Popover); - - // Application.ResetState (ignoreDisposed: true); - //} - - //[Fact] - //public void Popover_VisibleChangedEvent () - //{ - // // Arrange - // var popover = new View () - // { - // Visible = false - // }; - // Application.Popover = popover; - // bool eventTriggered = false; - - // popover.VisibleChanged += (sender, e) => eventTriggered = true; - - // // Act - // popover.Visible = true; - - // // Assert - // Assert.True (eventTriggered); - - // Application.ResetState (ignoreDisposed: true); - //} - - //[Fact] - //public void Popover_InitializesCorrectly () - //{ - // // Arrange - // var popover = new View (); - - // // Act - // Application.Popover = popover; - - // // Assert - // Assert.True (popover.IsInitialized); - - // Application.ResetState (ignoreDisposed: true); - //} - - //[Fact] - //public void Popover_SetsColorScheme () - //{ - // // Arrange - // var popover = new View (); - // var topColorScheme = new ColorScheme (); - // Application.Top = new Toplevel { ColorScheme = topColorScheme }; - - // // Act - // Application.Popover = popover; - - // // Assert - // Assert.Equal (topColorScheme, popover.ColorScheme); - - // Application.ResetState (ignoreDisposed: true); - //} - - //[Fact] - //public void Popover_VisibleChangedToTrue_SetsFocus () - //{ - // // Arrange - // var popover = new View () - // { - // Visible = false, - // CanFocus = true - // }; - // Application.Popover = popover; - - // // Act - // popover.Visible = true; - - // // Assert - // Assert.True (popover.Visible); - // Assert.True (popover.HasFocus); - - // Application.ResetState (ignoreDisposed: true); - //} - - //[Theory] - //[InlineData(-1, -1)] - //[InlineData (0, 0)] - //[InlineData (2048, 2048)] - //[InlineData (2049, 2049)] - //public void Popover_VisibleChangedToTrue_Locates_In_Visible_Position (int x, int y) - //{ - // // Arrange - // var popover = new View () - // { - // X = x, - // Y = y, - // Visible = false, - // CanFocus = true, - // Width = 1, - // Height = 1 - // }; - // Application.Popover = popover; - - // // Act - // popover.Visible = true; - // Application.LayoutAndDraw(); - - // // Assert - // Assert.True (Application.Screen.Contains (popover.Frame)); - - // Application.ResetState (ignoreDisposed: true); - //} - - //[Fact] - //public void Popover_VisibleChangedToFalse_Hides_And_Removes_Focus () - //{ - // // Arrange - // var popover = new View () - // { - // Visible = false, - // CanFocus = true - // }; - // Application.Popover = popover; - // popover.Visible = true; - - // // Act - // popover.Visible = false; - - // // Assert - // Assert.False (popover.Visible); - // Assert.False (popover.HasFocus); - - // Application.ResetState (ignoreDisposed: true); - //} - - //[Fact] - //public void Popover_Quit_Command_Hides () - //{ - // // Arrange - // var popover = new View () - // { - // Visible = false, - // CanFocus = true - // }; - // Application.Popover = popover; - // popover.Visible = true; - // Assert.True (popover.Visible); - // Assert.True (popover.HasFocus); - - // // Act - // Application.RaiseKeyDownEvent (Application.QuitKey); - - // // Assert - // Assert.False (popover.Visible); - // Assert.False (popover.HasFocus); - - // Application.ResetState (ignoreDisposed: true); - //} - - //[Fact] - //public void Popover_MouseClick_Outside_Hides_Passes_Event_On () - //{ - // // Arrange - // Application.Top = new Toplevel () - // { - // Id = "top", - // Height = 10, - // Width = 10, - // }; - - // View otherView = new () - // { - // X = 1, - // Y = 1, - // Height = 1, - // Width = 1, - // Id = "otherView", - // }; - - // bool otherViewPressed = false; - // otherView.MouseEvent += (sender, e) => - // { - // otherViewPressed = e.Flags.HasFlag(MouseFlags.Button1Pressed); - // }; - - // Application.Top.Add (otherView); - - // var popover = new View () - // { - // Id = "popover", - // X = 5, - // Y = 5, - // Width = 1, - // Height = 1, - // Visible = false, - // CanFocus = true - // }; - - // Application.Popover = popover; - // popover.Visible = true; - // Assert.True (popover.Visible); - // Assert.True (popover.HasFocus); - - // // Act - // // Click on popover - // Application.RaiseMouseEvent (new () { Flags = MouseFlags.Button1Pressed, ScreenPosition = new (5, 5) }); - // Assert.True (popover.Visible); - - // // Click outside popover (on button) - // Application.RaiseMouseEvent (new () { Flags = MouseFlags.Button1Pressed, ScreenPosition = new (1, 1) }); - - // // Assert - // Assert.True (otherViewPressed); - // Assert.False (popover.Visible); - - // Application.Top.Dispose (); - // Application.ResetState (ignoreDisposed: true); - //} - - //[Theory] - //[InlineData (0, 0, false)] - //[InlineData (5, 5, true)] - //[InlineData (10, 10, false)] - //[InlineData (5, 10, false)] - //[InlineData (9, 9, false)] - //public void Popover_MouseClick_Outside_Hides (int mouseX, int mouseY, bool expectedVisible) - //{ - // // Arrange - // Application.Top = new Toplevel () - // { - // Id = "top", - // Height = 10, - // Width = 10, - // }; - // var popover = new View () - // { - // Id = "popover", - // X = 5, - // Y = 5, - // Width = 1, - // Height = 1, - // Visible = false, - // CanFocus = true - // }; - - // Application.Popover = popover; - // popover.Visible = true; - // Assert.True (popover.Visible); - // Assert.True (popover.HasFocus); - - // // Act - // Application.RaiseMouseEvent (new () { Flags = MouseFlags.Button1Pressed, ScreenPosition = new (mouseX, mouseY) }); - - // // Assert - // Assert.Equal (expectedVisible, popover.Visible); - - // Application.Top.Dispose (); - // Application.ResetState (ignoreDisposed: true); - //} - - //[Fact] - //public void Popover_SetAndGet_ReturnsCorrectValue () - //{ - // // Arrange - // var view = new View (); - - // // Act - // Application.Popover = view; - - // // Assert - // Assert.Equal (view, Application.Popover); - - // Application.ResetState (ignoreDisposed: true); - //} - - //[Fact] - //public void Popover_SetToNull_HidesPreviousPopover () - //{ - // // Arrange - // var view = new View { Visible = true }; - // Application.Popover = view; - - // // Act - // Application.Popover = null; - - // // Assert - // Assert.False (view.Visible); - // Assert.Null (Application.Popover); - - // Application.ResetState (ignoreDisposed: true); - //} - - //[Fact] - //public void Popover_SetNewPopover_HidesPreviousPopover () - //{ - // // Arrange - // var oldView = new View { Visible = true }; - // var newView = new View (); - // Application.Popover = oldView; - - // // Act - // Application.Popover = newView; - - // // Assert - // Assert.False (oldView.Visible); - // Assert.Equal (newView, Application.Popover); - - // Application.ResetState (ignoreDisposed: true); - //} - - //[Fact] - //public void Popover_SetNewPopover_InitializesAndSetsProperties () - //{ - // // Arrange - // var view = new View (); - - // // Act - // Application.Popover = view; - - // // Assert - // Assert.True (view.IsInitialized); - // Assert.True (view.Arrangement.HasFlag (ViewArrangement.Overlapped)); - // Assert.Equal (Application.Top?.ColorScheme, view.ColorScheme); - - // Application.ResetState (ignoreDisposed: true); - //} + } diff --git a/Tests/UnitTests/Views/ContextMenuTests.cs b/Tests/UnitTests/Views/ContextMenuTests.cs deleted file mode 100644 index b6a69063e3..0000000000 --- a/Tests/UnitTests/Views/ContextMenuTests.cs +++ /dev/null @@ -1,2218 +0,0 @@ -using UnitTests; -using Xunit.Abstractions; - -namespace Terminal.Gui.ViewsTests; - -public class ContextMenuTests (ITestOutputHelper output) -{ - [Fact (Skip = "Redo for CMv2")] - [AutoInitShutdown] - public void ContextMenu_Constructors () - { - var cm = new ContextMenu (); - var top = new Toplevel (); - Application.Begin (top); - - Assert.Equal (Point.Empty, cm.Position); - Assert.Null (cm.MenuItems); - Assert.Null (cm.Host); - cm.Position = new Point (20, 10); - - var menuItems = new MenuBarItem ( - [ - new MenuItem ("First", "", null) - ] - ); - cm.Show (menuItems); - Assert.Equal (new Point (20, 10), cm.Position); - Assert.Single (cm.MenuItems!.Children); - - cm = new ContextMenu - { - Position = new Point (5, 10) - }; - - menuItems = new MenuBarItem ( - new [] { new MenuItem ("One", "", null), new MenuItem ("Two", "", null) } - ); - cm.Show (menuItems); - Assert.Equal (new Point (5, 10), cm.Position); - Assert.Equal (2, cm.MenuItems!.Children.Length); - Assert.Null (cm.Host); - - var view = new View { X = 5, Y = 10 }; - top.Add (view); - - cm = new ContextMenu - { - Host = view, - Position = new Point (5, 10) - }; - - menuItems = new MenuBarItem ( - new [] { new MenuItem ("One", "", null), new MenuItem ("Two", "", null) } - ); - cm.Show (menuItems); - Assert.Equal (new Point (5, 10), cm.Position); - Assert.Equal (2, cm.MenuItems.Children.Length); - Assert.NotNull (cm.Host); - - top.Dispose (); - } - - [Fact (Skip = "Redo for CMv2")] - [AutoInitShutdown] - public void ContextMenu_Is_Closed_If_Another_MenuBar_Is_Open_Or_Vice_Versa () - { - var cm = new ContextMenu - { - Position = new Point (10, 5) - }; - - var menuItems = new MenuBarItem ( - [ - new MenuItem ("One", "", null), - new MenuItem ("Two", "", null) - ] - ); - - var menuBar = new MenuBar - { - Menus = - [ - new MenuBarItem ("File", "", null), - new MenuBarItem ("Edit", "", null) - ] - }; - - var top = new Toplevel (); - top.Add (menuBar); - Application.Begin (top); - - Assert.Null (Application.MouseGrabView); - - cm.Show (menuItems); - Assert.True (ContextMenu.IsShow); - Menu menu = (Menu)top.SubViews.First (v => v is Menu); - Assert.Equal (menu, Application.MouseGrabView); - Assert.False (menuBar.IsMenuOpen); - Assert.True (menuBar.NewKeyDownEvent (menuBar.Key)); - Assert.False (ContextMenu.IsShow); - Assert.Equal (menuBar, Application.MouseGrabView); - Assert.True (menuBar.IsMenuOpen); - - cm.Show (menuItems); - Assert.True (ContextMenu.IsShow); - menu = (Menu)top.SubViews.First (v => v is Menu); - Assert.Equal (menu, Application.MouseGrabView); - Assert.False (menuBar.IsMenuOpen); -#if SUPPORT_ALT_TO_ACTIVATE_MENU - Assert.True (Application.Top.ProcessKeyUp (new (Key.AltMask))); - Assert.False (ContextMenu.IsShow); - Assert.Equal (menu, Application.MouseGrabView); - Assert.True (menu.IsMenuOpen); -#endif - - cm.Show (menuItems); - Assert.True (ContextMenu.IsShow); - menu = (Menu)top.SubViews.First (v => v is Menu); - Assert.Equal (menu, Application.MouseGrabView); - Assert.False (menuBar.IsMenuOpen); - Assert.False (menuBar.NewMouseEvent (new MouseEventArgs { Position = new (1, 0), Flags = MouseFlags.ReportMousePosition, View = menuBar })); - Assert.True (ContextMenu.IsShow); - Assert.Equal (menu, Application.MouseGrabView); - Assert.False (menuBar.IsMenuOpen); - Assert.True (menuBar.NewMouseEvent (new MouseEventArgs { Position = new (1, 0), Flags = MouseFlags.Button1Clicked, View = menuBar })); - Assert.False (ContextMenu.IsShow); - Assert.Equal (menuBar, Application.MouseGrabView); - Assert.True (menuBar.IsMenuOpen); - top.Dispose (); - } - - [Fact (Skip = "#3798 Broke. Will fix in #2975")] - [AutoInitShutdown] - public void Draw_A_ContextMenu_Over_A_Borderless_Top () - { - ((FakeDriver)Application.Driver!).SetBufferSize (20, 15); - - Assert.Equal (new Rectangle (0, 0, 20, 15), View.GetClip ()!.GetBounds ()); - DriverAssert.AssertDriverContentsWithFrameAre ("", output); - - var top = new Toplevel { X = 2, Y = 2, Width = 15, Height = 4 }; - top.Add (new TextField { X = Pos.Center (), Width = 10, Text = "Test" }); - RunState rs = Application.Begin (top); - Application.RunIteration (ref rs); - - Assert.Equal (new Rectangle (2, 2, 15, 4), top.Frame); - Assert.Equal (top, Application.Top); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - Test", - output - ); - - Application.RaiseMouseEvent (new MouseEventArgs { ScreenPosition = new (8, 2), Flags = MouseFlags.Button3Clicked }); - - Application.RunIteration (ref rs); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - Test -┌─────────────────── -│ Select All Ctrl+ -│ Delete All Ctrl+ -│ Copy Ctrl+ -│ Cut Ctrl+ -│ Paste Ctrl+ -│ Undo Ctrl+ -│ Redo Ctrl+ -└───────────────────", - output - ); - - Application.End (rs); - top.Dispose (); - } - - [Fact (Skip = "#3798 Broke. Will fix in #2975")] - [AutoInitShutdown] - public void Draw_A_ContextMenu_Over_A_Dialog () - { - Toplevel top = new (); - var win = new Window (); - top.Add (win); - RunState rsTop = Application.Begin (top); - ((FakeDriver)Application.Driver!).SetBufferSize (20, 15); - - Assert.Equal (new Rectangle (0, 0, 20, 15), win.Frame); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────┐ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -└──────────────────┘", - output - ); - - // Don't use Dialog here as it has more layout logic. Use Window instead. - var testWindow = new Window { X = 2, Y = 2, Width = 15, Height = 4 }; - testWindow.Add (new TextField { X = Pos.Center (), Width = 10, Text = "Test" }); - RunState rsDialog = Application.Begin (testWindow); - Application.LayoutAndDraw (); - - Assert.Equal (new Rectangle (2, 2, 15, 4), testWindow.Frame); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────┐ -│ │ -│ ┌─────────────┐ │ -│ │ Test │ │ -│ │ │ │ -│ └─────────────┘ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -└──────────────────┘", - output - ); - - Application.RaiseMouseEvent (new MouseEventArgs { ScreenPosition = new (9, 3), Flags = MouseFlags.Button3Clicked }); - - Application.RunIteration (ref rsDialog); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────┐ -│ │ -│ ┌─────────────┐ │ -│ │ Test │ │ -┌─────────────────── -│ Select All Ctrl+ -│ Delete All Ctrl+ -│ Copy Ctrl+ -│ Cut Ctrl+ -│ Paste Ctrl+ -│ Undo Ctrl+ -│ Redo Ctrl+ -└─────────────────── -│ │ -└──────────────────┘", - output - ); - - Application.End (rsDialog); - Application.End (rsTop); - top.Dispose (); - } - - [Fact (Skip = "#3798 Broke. Will fix in #2975")] - [AutoInitShutdown] - public void Draw_A_ContextMenu_Over_A_Top_Dialog () - { - ((FakeDriver)Application.Driver!).SetBufferSize (20, 15); - - Assert.Equal (new Rectangle (0, 0, 20, 15), View.GetClip ()!.GetBounds ()); - DriverAssert.AssertDriverContentsWithFrameAre ("", output); - - // Don't use Dialog here as it has more layout logic. Use Window instead. - var dialog = new Window { X = 2, Y = 2, Width = 15, Height = 4 }; - dialog.Add (new TextField { X = Pos.Center (), Width = 10, Text = "Test" }); - RunState rs = Application.Begin (dialog); - Application.LayoutAndDraw (); - - Assert.Equal (new Rectangle (2, 2, 15, 4), dialog.Frame); - Assert.Equal (dialog, Application.Top); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌─────────────┐ - │ Test │ - │ │ - └─────────────┘", - output - ); - - Application.RaiseMouseEvent (new MouseEventArgs { ScreenPosition = new (9, 3), Flags = MouseFlags.Button3Clicked }); - - var firstIteration = false; - Application.RunIteration (ref rs, firstIteration); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌─────────────┐ - │ Test │ -┌─────────────────── -│ Select All Ctrl+ -│ Delete All Ctrl+ -│ Copy Ctrl+ -│ Cut Ctrl+ -│ Paste Ctrl+ -│ Undo Ctrl+ -│ Redo Ctrl+ -└───────────────────", - output - ); - - Application.End (rs); - dialog.Dispose (); - } - - [Fact (Skip = "Redo for CMv2")] - [AutoInitShutdown] - public void ForceMinimumPosToZero_True_False () - { - var cm = new ContextMenu - { - Position = new Point (-1, -2) - }; - - var menuItems = new MenuBarItem ( - [ - new MenuItem ("One", "", null), - new MenuItem ("Two", "", null) - ] - ); - Assert.Equal (new Point (-1, -2), cm.Position); - - Toplevel top = new (); - Application.Begin (top); - - cm.Show (menuItems); - Assert.Equal (new Point (-1, -2), cm.Position); - Application.LayoutAndDraw (); - - var expected = @" -┌──────┐ -│ One │ -│ Two │ -└──────┘ -"; - - Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rectangle (0, 1, 8, 4), pos); - - cm.ForceMinimumPosToZero = false; - cm.Show (menuItems); - Assert.Equal (new Point (-1, -2), cm.Position); - Application.LayoutAndDraw (); - - expected = @" - One │ - Two │ -──────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rectangle (1, 0, 7, 3), pos); - top.Dispose (); - } - - [Fact (Skip = "Redo for CMv2")] - [AutoInitShutdown] - public void Hide_Is_Invoke_At_Container_Closing () - { - var cm = new ContextMenu - { - Position = new Point (80, 25) - }; - - var menuItems = new MenuBarItem ( - [ - new MenuItem ("One", "", null), - new MenuItem ("Two", "", null) - ] - ); - Toplevel top = new (); - Application.Begin (top); - top.Running = true; - - Assert.False (ContextMenu.IsShow); - - cm.Show (menuItems); - Assert.True (ContextMenu.IsShow); - - top.RequestStop (); - Assert.False (ContextMenu.IsShow); - top.Dispose (); - } - - //[Fact (Skip = "Redo for CMv2")] - //[AutoInitShutdown] - //public void Key_Open_And_Close_The_ContextMenu () - //{ - // var tf = new TextField (); - // var top = new Toplevel (); - // top.Add (tf); - // Application.Begin (top); - - // Assert.True (Application.RaiseKeyDownEvent (ContextMenu.DefaultKey)); - // Assert.True (tf.ContextMenu.MenuBar!.IsMenuOpen); - // Assert.True (Application.RaiseKeyDownEvent (ContextMenu.DefaultKey)); - - // // The last context menu bar opened is always preserved - // Assert.False (tf.ContextMenu.Visible); - // top.Dispose (); - //} - - [Fact (Skip = "Redo for CMv2")] - [AutoInitShutdown] - public void KeyChanged_Event () - { - var oldKey = Key.Empty; - var cm = new ContextMenu (); - - cm.KeyChanged += (s, e) => oldKey = e.OldKey; - - cm.Key = Key.Space.WithCtrl; - Assert.Equal (Key.Space.WithCtrl, cm.Key); - Assert.Equal (ContextMenu.DefaultKey, oldKey); - } - - [Fact (Skip = "Redo for CMv2")] - [AutoInitShutdown] - public void MenuItens_Changing () - { - var cm = new ContextMenu - { - Position = new Point (10, 5) - }; - - var menuItems = new MenuBarItem ( - [ - new MenuItem ("One", "", null), - new MenuItem ("Two", "", null) - ] - ); - Toplevel top = new (); - Application.Begin (top); - cm.Show (menuItems); - Application.LayoutAndDraw (); - - var expected = @" - ┌──────┐ - │ One │ - │ Two │ - └──────┘ -"; - - DriverAssert.AssertDriverContentsAre (expected, output); - - menuItems = new MenuBarItem ( - [ - new MenuItem ("First", "", null), - new MenuItem ("Second", "", null), - new MenuItem ("Third", "", null) - ] - ); - - cm.Show (menuItems); - Application.LayoutAndDraw (); - - expected = @" - ┌─────────┐ - │ First │ - │ Second │ - │ Third │ - └─────────┘ -"; - - DriverAssert.AssertDriverContentsAre (expected, output); - top.Dispose (); - } - - [Fact (Skip = "Redo for CMv2")] - [AutoInitShutdown] - public void Menus_And_SubMenus_Always_Try_To_Be_On_Screen () - { - var cm = new ContextMenu - { - Position = new Point (-1, -2) - }; - - var menuItems = new MenuBarItem ( - [ - new MenuItem ("One", "", null), - new MenuItem ("Two", "", null), - new MenuItem ("Three", "", null), - new MenuBarItem ( - "Four", - [ - new MenuItem ("SubMenu1", "", null), - new MenuItem ("SubMenu2", "", null), - new MenuItem ("SubMenu3", "", null), - new MenuItem ("SubMenu4", "", null), - new MenuItem ("SubMenu5", "", null), - new MenuItem ("SubMenu6", "", null), - new MenuItem ("SubMenu7", "", null) - ] - ), - new MenuItem ("Five", "", null), - new MenuItem ("Six", "", null) - ] - ); - Assert.Equal (new Point (-1, -2), cm.Position); - - Toplevel top = new (); - RunState rs = Application.Begin (top); - - cm.Show (menuItems); - Application.RunIteration (ref rs); - - Assert.Equal (new Point (-1, -2), cm.Position); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌────────┐ -│ One │ -│ Two │ -│ Three │ -│ Four ►│ -│ Five │ -│ Six │ -└────────┘ -", - output - ); - - View menu = top.SubViews.First (v => v is Menu); - - Assert.True ( - menu - .NewMouseEvent ( - new MouseEventArgs { Position = new (0, 3), Flags = MouseFlags.ReportMousePosition, View = menu } - ) - ); - Application.RunIteration (ref rs); - Assert.Equal (new Point (-1, -2), cm.Position); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌────────┐ -│ One │ -│ Two │ -│ Three │ -│ Four ►│┌───────────┐ -│ Five ││ SubMenu1 │ -│ Six ││ SubMenu2 │ -└────────┘│ SubMenu3 │ - │ SubMenu4 │ - │ SubMenu5 │ - │ SubMenu6 │ - │ SubMenu7 │ - └───────────┘ -", - output - ); - - ((FakeDriver)Application.Driver!).SetBufferSize (40, 20); - cm.Position = new Point (41, -2); - cm.Show (menuItems); - Application.RunIteration (ref rs); - Assert.Equal (new Point (41, -2), cm.Position); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌────────┐ - │ One │ - │ Two │ - │ Three │ - │ Four ►│ - │ Five │ - │ Six │ - └────────┘ -", - output - ); - - menu = top.SubViews.First (v => v is Menu); - Assert.True ( - menu - .NewMouseEvent ( - new MouseEventArgs { Position = new (30, 3), Flags = MouseFlags.ReportMousePosition, View = menu } - ) - ); - Application.RunIteration (ref rs); - Assert.Equal (new Point (41, -2), cm.Position); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌────────┐ - │ One │ - │ Two │ - │ Three │ - ┌───────────┐│ Four ►│ - │ SubMenu1 ││ Five │ - │ SubMenu2 ││ Six │ - │ SubMenu3 │└────────┘ - │ SubMenu4 │ - │ SubMenu5 │ - │ SubMenu6 │ - │ SubMenu7 │ - └───────────┘ -", - output - ); - - cm.Position = new Point (41, 9); - cm.Show (menuItems); - Application.RunIteration (ref rs); - Assert.Equal (new Point (41, 9), cm.Position); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌────────┐ - │ One │ - │ Two │ - │ Three │ - │ Four ►│ - │ Five │ - │ Six │ - └────────┘ -", - output - ); - - menu = top.SubViews.First (v => v is Menu); - Assert.True ( - menu - .NewMouseEvent ( - new MouseEventArgs { Position = new (30, 3), Flags = MouseFlags.ReportMousePosition, View = menu } - ) - ); - Application.RunIteration (ref rs); - Assert.Equal (new Point (41, 9), cm.Position); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌────────┐ - ┌───────────┐│ One │ - │ SubMenu1 ││ Two │ - │ SubMenu2 ││ Three │ - │ SubMenu3 ││ Four ►│ - │ SubMenu4 ││ Five │ - │ SubMenu5 ││ Six │ - │ SubMenu6 │└────────┘ - │ SubMenu7 │ - └───────────┘ -", - output - ); - - cm.Position = new Point (41, 22); - cm.Show (menuItems); - Application.RunIteration (ref rs); - Assert.Equal (new Point (41, 22), cm.Position); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌────────┐ - │ One │ - │ Two │ - │ Three │ - │ Four ►│ - │ Five │ - │ Six │ - └────────┘ -", - output - ); - - menu = top.SubViews.First (v => v is Menu); - Assert.True ( - menu - .NewMouseEvent ( - new MouseEventArgs { Position = new (30, 3), Flags = MouseFlags.ReportMousePosition, View = menu } - ) - ); - Application.RunIteration (ref rs); - Assert.Equal (new Point (41, 22), cm.Position); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌───────────┐ - │ SubMenu1 │┌────────┐ - │ SubMenu2 ││ One │ - │ SubMenu3 ││ Two │ - │ SubMenu4 ││ Three │ - │ SubMenu5 ││ Four ►│ - │ SubMenu6 ││ Five │ - │ SubMenu7 ││ Six │ - └───────────┘└────────┘ -", - output - ); - - ((FakeDriver)Application.Driver!).SetBufferSize (18, 8); - cm.Position = new Point (19, 10); - cm.Show (menuItems); - Application.RunIteration (ref rs); - Assert.Equal (new Point (19, 10), cm.Position); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌────────┐ - │ One │ - │ Two │ - │ Three │ - │ Four ►│ - │ Five │ - │ Six │ - └────────┘ -", - output - ); - - menu = top.SubViews.First (v => v is Menu); - Assert.True ( - menu - .NewMouseEvent ( - new MouseEventArgs { Position = new (30, 3), Flags = MouseFlags.ReportMousePosition, View = menu } - ) - ); - Application.RunIteration (ref rs); - Assert.Equal (new Point (19, 10), cm.Position); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌───────────┐────┐ -│ SubMenu1 │ │ -│ SubMenu2 │ │ -│ SubMenu3 │ee │ -│ SubMenu4 │r ►│ -│ SubMenu5 │e │ -│ SubMenu6 │ │ -│ SubMenu7 │────┘ -", - output - ); - top.Dispose (); - } - - [Fact (Skip = "Redo for CMv2")] - [AutoInitShutdown] - public void MouseFlags_Changing () - { - var lbl = new Label { Text = "Original" }; - - var cm = new ContextMenu (); - - lbl.MouseClick += (s, e) => - { - if (e.Flags == cm.MouseFlags) - { - lbl.Text = "Replaced"; - e.Handled = true; - } - }; - - Toplevel top = new (); - top.Add (lbl); - Application.Begin (top); - - Assert.True (lbl.NewMouseEvent (new MouseEventArgs { Flags = cm.MouseFlags })); - Assert.Equal ("Replaced", lbl.Text); - - lbl.Text = "Original"; - cm.MouseFlags = MouseFlags.Button2Clicked; - Assert.True (lbl.NewMouseEvent (new MouseEventArgs { Flags = cm.MouseFlags })); - Assert.Equal ("Replaced", lbl.Text); - top.Dispose (); - } - - [Fact (Skip = "Redo for CMv2")] - public void MouseFlagsChanged_Event () - { - var oldMouseFlags = new MouseFlags (); - var cm = new ContextMenu (); - - cm.MouseFlagsChanged += (s, e) => oldMouseFlags = e.OldValue; - - cm.MouseFlags = MouseFlags.Button2Clicked; - Assert.Equal (MouseFlags.Button2Clicked, cm.MouseFlags); - Assert.Equal (MouseFlags.Button3Clicked, oldMouseFlags); - } - - [Fact (Skip = "Redo for CMv2")] - [AutoInitShutdown] - public void Position_Changing () - { - var cm = new ContextMenu - { - Position = new Point (10, 5) - }; - - var menuItems = new MenuBarItem ( - [ - new MenuItem ("One", "", null), - new MenuItem ("Two", "", null) - ] - ); - Toplevel top = new (); - Application.Begin (top); - cm.Show (menuItems); - Application.LayoutAndDraw (); - - var expected = @" - ┌──────┐ - │ One │ - │ Two │ - └──────┘ -"; - - DriverAssert.AssertDriverContentsAre (expected, output); - - cm.Position = new Point (5, 10); - - cm.Show (menuItems); - Application.LayoutAndDraw (); - - expected = @" - ┌──────┐ - │ One │ - │ Two │ - └──────┘ -"; - - DriverAssert.AssertDriverContentsAre (expected, output); - top.Dispose (); - } - - [Fact (Skip = "Redo for CMv2")] - [AutoInitShutdown] - public void RequestStop_While_ContextMenu_Is_Open_Does_Not_Throws () - { - ContextMenu cm = new ContextMenu - { - Position = new Point (10, 5) - }; - - var menuItems = new MenuBarItem ( - new MenuItem [] { new ("One", "", null), new ("Two", "", null) } - ); - Toplevel top = new (); - var isMenuAllClosed = false; - MenuBarItem mi = null; - int iterations = -1; - - Application.Iteration += (s, a) => - { - iterations++; - - if (iterations == 0) - { - cm.Show (menuItems); - Assert.True (ContextMenu.IsShow); - mi = cm.MenuBar.Menus [0]; - - mi.Action = () => - { - Assert.True (ContextMenu.IsShow); - - var dialog1 = new Dialog () { Id = "dialog1" }; - Application.Run (dialog1); - dialog1.Dispose (); - Assert.False (ContextMenu.IsShow); - Assert.True (isMenuAllClosed); - }; - cm.MenuBar.MenuAllClosed += (_, _) => isMenuAllClosed = true; - } - else if (iterations == 1) - { - mi.Action (); - } - else if (iterations == 2) - { - Application.RequestStop (); - } - else if (iterations == 3) - { - isMenuAllClosed = false; - cm.Show (menuItems); - Assert.True (ContextMenu.IsShow); - cm.MenuBar.MenuAllClosed += (_, _) => isMenuAllClosed = true; - } - else if (iterations == 4) - { - Exception exception = Record.Exception (() => Application.RequestStop ()); - Assert.Null (exception); - } - else - { - Application.RequestStop (); - } - }; - - var isTopClosed = false; - - top.Closing += (_, _) => - { - var dialog2 = new Dialog () { Id = "dialog2" }; - Application.Run (dialog2); - dialog2.Dispose (); - Assert.False (ContextMenu.IsShow); - Assert.True (isMenuAllClosed); - isTopClosed = true; - }; - - Application.Run (top); - - Assert.True (isTopClosed); - Assert.False (ContextMenu.IsShow); - Assert.True (isMenuAllClosed); - top.Dispose (); - } - - [Fact (Skip = "Redo for CMv2")] - [AutoInitShutdown] - public void Show_Display_At_Zero_If_The_Toplevel_Height_Is_Less_Than_The_Menu_Height () - { - ((FakeDriver)Application.Driver!).SetBufferSize (80, 3); - - var cm = new ContextMenu - { - Position = Point.Empty - }; - - var menuItems = new MenuBarItem ( - [ - new MenuItem ("One", "", null), - new MenuItem ("Two", "", null) - ] - ); - Assert.Equal (Point.Empty, cm.Position); - - Toplevel top = new (); - Application.Begin (top); - cm.Show (menuItems); - Assert.Equal (Point.Empty, cm.Position); - Application.LayoutAndDraw (); - - var expected = @" -┌──────┐ -│ One │ -│ Two │"; - - Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rectangle (0, 0, 8, 3), pos); - - cm.Hide (); - Assert.Equal (Point.Empty, cm.Position); - top.Dispose (); - } - - [Fact (Skip = "Redo for CMv2")] - [AutoInitShutdown] - public void Show_Display_At_Zero_If_The_Toplevel_Width_Is_Less_Than_The_Menu_Width () - { - ((FakeDriver)Application.Driver!).SetBufferSize (5, 25); - - var cm = new ContextMenu - { - Position = Point.Empty - }; - - var menuItems = new MenuBarItem ( - [ - new MenuItem ("One", "", null), - new MenuItem ("Two", "", null) - ] - ); - Assert.Equal (Point.Empty, cm.Position); - - Toplevel top = new (); - Application.Begin (top); - cm.Show (menuItems); - Assert.Equal (Point.Empty, cm.Position); - Application.LayoutAndDraw (); - - var expected = @" -┌──── -│ One -│ Two -└────"; - - Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rectangle (0, 1, 5, 4), pos); - - cm.Hide (); - Assert.Equal (Point.Empty, cm.Position); - top.Dispose (); - } - - [Fact (Skip = "Redo for CMv2")] - [AutoInitShutdown] - public void Show_Display_Below_The_Bottom_Host_If_Has_Enough_Space () - { - var view = new View - { - X = 10, - Y = 5, - Width = 10, - Height = 1, - Text = "View" - }; - - var cm = new ContextMenu - { - Host = view, - Position = new Point (10, 5) - }; - - var menuItems = new MenuBarItem ( - [ - new MenuItem ("One", "", null), - new MenuItem ("Two", "", null) - ] - ); - var top = new Toplevel (); - top.Add (view); - Application.Begin (top); - - Assert.Equal (new Point (10, 5), cm.Position); - - cm.Show (menuItems); - top.Draw (); - Assert.Equal (new Point (10, 5), cm.Position); - - var expected = @" - View - ┌──────┐ - │ One │ - │ Two │ - └──────┘ -"; - - Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rectangle (10, 5, 18, 5), pos); - - cm.Hide (); - Assert.Equal (new Point (10, 5), cm.Position); - cm.Host.X = 5; - cm.Host.Y = 10; - cm.Host.Height = 3; - - cm.Show (menuItems); - View.SetClipToScreen (); - Application.Top.Draw (); - Assert.Equal (new Point (5, 12), cm.Position); - - expected = @" - View - - - ┌──────┐ - │ One │ - │ Two │ - └──────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rectangle (5, 10, 13, 7), pos); - - cm.Hide (); - Assert.Equal (new Point (5, 12), cm.Position); - top.Dispose (); - } - - [Fact (Skip = "Redo for CMv2")] - [AutoInitShutdown] - public void Show_Ensures_Display_Inside_The_Container_But_Preserves_Position () - { - var cm = new ContextMenu - { - Position = new Point (80, 25) - }; - - var menuItems = new MenuBarItem ( - [ - new MenuItem ("One", "", null), - new MenuItem ("Two", "", null) - ] - ); - Assert.Equal (new Point (80, 25), cm.Position); - - Toplevel top = new (); - Application.Begin (top); - cm.Show (menuItems); - Assert.Equal (new Point (80, 25), cm.Position); - Application.LayoutAndDraw (); - - var expected = @" - ┌──────┐ - │ One │ - │ Two │ - └──────┘ -"; - - Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rectangle (72, 21, 80, 4), pos); - - cm.Hide (); - Assert.Equal (new Point (80, 25), cm.Position); - top.Dispose (); - } - - [Fact (Skip = "Redo for CMv2")] - [AutoInitShutdown] - public void Show_Ensures_Display_Inside_The_Container_Without_Overlap_The_Host () - { - var view = new View - { - X = Pos.AnchorEnd (10), - Y = Pos.AnchorEnd (1), - Width = 10, - Height = 1, - Text = "View" - }; - - var cm = new ContextMenu - { - Host = view - }; - - var menuItems = new MenuBarItem ( - [ - new MenuItem ("One", "", null), - new MenuItem ("Two", "", null) - ] - ); - var top = new Toplevel (); - top.Add (view); - Application.Begin (top); - - Assert.Equal (new Rectangle (70, 24, 10, 1), view.Frame); - Assert.Equal (Point.Empty, cm.Position); - - cm.Show (menuItems); - Assert.Equal (new Point (70, 24), cm.Position); - top.Draw (); - - var expected = @" - ┌──────┐ - │ One │ - │ Two │ - └──────┘ - View -"; - - Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new Rectangle (70, 20, 78, 5), pos); - - cm.Hide (); - Assert.Equal (new Point (70, 24), cm.Position); - top.Dispose (); - } - - [Fact (Skip = "Redo for CMv2")] - [AutoInitShutdown] - public void Show_Hide_IsShow () - { - ContextMenu cm = new ContextMenu - { - Position = new Point (10, 5) - }; - - var menuItems = new MenuBarItem ( - new MenuItem [] { new ("One", "", null), new ("Two", "", null) } - ); - - Toplevel top = new (); - Application.Begin (top); - cm.Show (menuItems); - Assert.True (ContextMenu.IsShow); - Application.LayoutAndDraw (); - - var expected = @" - ┌──────┐ - │ One │ - │ Two │ - └──────┘ -"; - - DriverAssert.AssertDriverContentsAre (expected, output); - - cm.Hide (); - Assert.False (ContextMenu.IsShow); - - Application.LayoutAndDraw (); - - expected = ""; - - DriverAssert.AssertDriverContentsAre (expected, output); - top.Dispose (); - } - - [Fact (Skip = "Redo for CMv2")] - [AutoInitShutdown] - public void UseSubMenusSingleFrame_True_By_Mouse () - { - var cm = new ContextMenu - { - Position = new Point (5, 10), - UseSubMenusSingleFrame = true - }; - - var menuItems = new MenuBarItem ( - "Numbers", - [ - new MenuItem ("One", "", null), - new MenuBarItem ( - "Two", - [ - new MenuItem ( - "Sub-Menu 1", - "", - null - ), - new MenuItem ("Sub-Menu 2", "", null) - ] - ), - new MenuItem ("Three", "", null) - ] - ); - Toplevel top = new (); - RunState rs = Application.Begin (top); - cm.Show (menuItems); - var menu = Application.Top!.SubViews.First (v => v is Menu); - Assert.Equal (new Rectangle (5, 11, 10, 5), menu.Frame); - Application.LayoutAndDraw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌────────┐ - │ One │ - │ Two ►│ - │ Three │ - └────────┘", - output - ); - - // X=5 is the border and so need to use at least one more - Application.RaiseMouseEvent (new MouseEventArgs { ScreenPosition = new (6, 13), Flags = MouseFlags.Button1Clicked }); - - var firstIteration = false; - Application.RunIteration (ref rs, firstIteration); - menu = Application.Top!.SubViews.First (v => v is Menu); - Assert.Equal (new Rectangle (5, 11, 10, 5), menu.Frame); - menu = Application.Top!.SubViews.Last (v => v is Menu); - Assert.Equal (new Rectangle (5, 11, 15, 6), menu.Frame); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌─────────────┐ - │◄ Two │ - ├─────────────┤ - │ Sub-Menu 1 │ - │ Sub-Menu 2 │ - └─────────────┘", - output - ); - - Application.RaiseMouseEvent (new MouseEventArgs { ScreenPosition = new (6, 12), Flags = MouseFlags.Button1Clicked }); - - firstIteration = false; - Application.RunIteration (ref rs, firstIteration); - menu = Application.Top!.SubViews.First (v => v is Menu); - Assert.Equal (new Rectangle (5, 11, 10, 5), menu.Frame); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌────────┐ - │ One │ - │ Two ►│ - │ Three │ - └────────┘", - output - ); - - Application.End (rs); - top.Dispose (); - } - - [Fact (Skip = "Redo for CMv2")] - [AutoInitShutdown] - public void UseSubMenusSingleFrame_False_By_Mouse () - { - var cm = new ContextMenu - { - Position = new Point (5, 10) - }; - - var menuItems = new MenuBarItem ( - "Numbers", - [ - new MenuItem ("One", "", null), - new MenuBarItem ( - "Two", - [ - new MenuItem ( - "Two-Menu 1", - "", - null - ), - new MenuItem ("Two-Menu 2", "", null) - ] - ), - new MenuBarItem ( - "Three", - [ - new MenuItem ( - "Three-Menu 1", - "", - null - ), - new MenuItem ("Three-Menu 2", "", null) - ] - ) - ] - ); - Toplevel top = new (); - RunState rs = Application.Begin (top); - cm.Show (menuItems); - - - var menu = Application.Top!.SubViews.First (v => v is Menu); - - Assert.Equal (new Rectangle (5, 11, 10, 5), menu.Frame); - Application.LayoutAndDraw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌────────┐ - │ One │ - │ Two ►│ - │ Three ►│ - └────────┘", - output - ); - - Application.RaiseMouseEvent (new MouseEventArgs { ScreenPosition = new (6, 13), Flags = MouseFlags.ReportMousePosition }); - - var firstIteration = false; - Application.RunIteration (ref rs, firstIteration); - menu = Application.Top!.SubViews.First (v => v is Menu); - Assert.Equal (new Rectangle (5, 11, 10, 5), menu.Frame); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌────────┐ - │ One │ - │ Two ►│┌─────────────┐ - │ Three ►││ Two-Menu 1 │ - └────────┘│ Two-Menu 2 │ - └─────────────┘", - output - ); - - Application.RaiseMouseEvent (new MouseEventArgs { ScreenPosition = new (6, 14), Flags = MouseFlags.ReportMousePosition }); - - firstIteration = false; - Application.RunIteration (ref rs, firstIteration); - menu = Application.Top!.SubViews.First (v => v is Menu); - Assert.Equal (new Rectangle (5, 11, 10, 5), menu.Frame); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌────────┐ - │ One │ - │ Two ►│ - │ Three ►│┌───────────────┐ - └────────┘│ Three-Menu 1 │ - │ Three-Menu 2 │ - └───────────────┘", - output - ); - - Application.RaiseMouseEvent (new MouseEventArgs { ScreenPosition = new (6, 13), Flags = MouseFlags.ReportMousePosition }); - - firstIteration = false; - Application.RunIteration (ref rs, firstIteration); - menu = Application.Top!.SubViews.First (v => v is Menu); - Assert.Equal (new Rectangle (5, 11, 10, 5), menu.Frame); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌────────┐ - │ One │ - │ Two ►│┌─────────────┐ - │ Three ►││ Two-Menu 1 │ - └────────┘│ Two-Menu 2 │ - └─────────────┘", - output - ); - - Application.End (rs); - top.Dispose (); - } - - [Fact (Skip = "Redo for CMv2")] - [AutoInitShutdown] - public void Handling_TextField_With_Opened_ContextMenu_By_Mouse_HasFocus () - { - var tf1 = new TextField { Width = 10, Text = "TextField 1" }; - var tf2 = new TextField { Y = 2, Width = 10, Text = "TextField 2" }; - var win = new Window (); - win.Add (tf1, tf2); - var rs = Application.Begin (win); - - Assert.True (tf1.HasFocus); - Assert.False (tf2.HasFocus); - Assert.Equal (4, win.SubViews.Count); // TF & TV add autocomplete popup's to their superviews. - Assert.Empty (Application._cachedViewsUnderMouse); - - // Right click on tf2 to open context menu - Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 3), Flags = MouseFlags.Button3Clicked }); - Assert.False (tf1.HasFocus); - Assert.False (tf2.HasFocus); - Assert.Equal (6, win.SubViews.Count); - //Assert.True (tf2.ContextMenu.IsMenuOpen); - Assert.True (win.Focused is Menu); - Assert.True (Application.MouseGrabView is Menu); - Assert.Equal (tf2, Application._cachedViewsUnderMouse.LastOrDefault ()); - - // Click on tf1 to focus it, which cause context menu being closed - Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 1), Flags = MouseFlags.Button1Clicked }); - Assert.True (tf1.HasFocus); - Assert.False (tf2.HasFocus); - Assert.Equal (5, win.SubViews.Count); - - // The last context menu bar opened is always preserved - Assert.NotNull (tf2.ContextMenu); - Assert.Equal (win.Focused, tf1); - Assert.Null (Application.MouseGrabView); - Assert.Equal (tf1, Application._cachedViewsUnderMouse.LastOrDefault ()); - - // Click on tf2 to focus it - Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 3), Flags = MouseFlags.Button1Clicked }); - Assert.False (tf1.HasFocus); - Assert.True (tf2.HasFocus); - Assert.Equal (5, win.SubViews.Count); - - // The last context menu bar opened is always preserved - Assert.NotNull (tf2.ContextMenu); - Assert.Equal (win.Focused, tf2); - Assert.Null (Application.MouseGrabView); - Assert.Equal (tf2, Application._cachedViewsUnderMouse.LastOrDefault ()); - - Application.End (rs); - win.Dispose (); - } - - [Fact (Skip = "Redo for CMv2")] - [AutoInitShutdown] - public void Empty_Menus_Items_Children_Does_Not_Open_The_Menu () - { - var cm = new ContextMenu (); - Assert.Null (cm.MenuItems); - - var top = new Toplevel (); - Application.Begin (top); - - cm.Show (cm.MenuItems); - Assert.Null (cm.MenuBar); - - top.Dispose (); - } - - [Fact (Skip = "Redo for CMv2")] - [AutoInitShutdown] - public void KeyBindings_Removed_On_Close_ContextMenu () - { - var newFile = false; - var renameFile = false; - var deleteFile = false; - - var cm = new ContextMenu (); - - var menuItems = new MenuBarItem ( - [ - new MenuItem ("New File", string.Empty, New, null, null, Key.N.WithCtrl), - new MenuItem ("Rename File", string.Empty, Rename, null, null, Key.R.WithCtrl), - new MenuItem ("Delete File", string.Empty, Delete, null, null, Key.D.WithCtrl) - ] - ); - var top = new Toplevel (); - Application.Begin (top); - - Assert.Null (cm.MenuBar); - Assert.False (Application.RaiseKeyDownEvent (Key.N.WithCtrl)); - Assert.False (Application.RaiseKeyDownEvent (Key.R.WithCtrl)); - Assert.False (Application.RaiseKeyDownEvent (Key.D.WithCtrl)); - Assert.False (newFile); - Assert.False (renameFile); - Assert.False (deleteFile); - - cm.Show (menuItems); - Assert.True (cm.MenuBar!.HotKeyBindings.TryGet (Key.N.WithCtrl, out _)); - Assert.True (cm.MenuBar.HotKeyBindings.TryGet (Key.R.WithCtrl, out _)); - Assert.True (cm.MenuBar.HotKeyBindings.TryGet (Key.D.WithCtrl, out _)); - - Assert.True (Application.RaiseKeyDownEvent (Key.N.WithCtrl)); - Application.MainLoop!.RunIteration (); - Assert.True (newFile); - Assert.False (cm.MenuBar!.IsMenuOpen); - cm.Show (menuItems); - Assert.True (Application.RaiseKeyDownEvent (Key.R.WithCtrl)); - Application.MainLoop!.RunIteration (); - Assert.True (renameFile); - Assert.False (cm.MenuBar.IsMenuOpen); - cm.Show (menuItems); - Assert.True (Application.RaiseKeyDownEvent (Key.D.WithCtrl)); - Application.MainLoop!.RunIteration (); - Assert.True (deleteFile); - Assert.False (cm.MenuBar.IsMenuOpen); - - Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _)); - Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.WithCtrl, out _)); - Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.D.WithCtrl, out _)); - - newFile = false; - renameFile = false; - deleteFile = false; - Assert.False (Application.RaiseKeyDownEvent (Key.N.WithCtrl)); - Assert.False (Application.RaiseKeyDownEvent (Key.R.WithCtrl)); - Assert.False (Application.RaiseKeyDownEvent (Key.D.WithCtrl)); - Assert.False (newFile); - Assert.False (renameFile); - Assert.False (deleteFile); - - top.Dispose (); - - void New () { newFile = true; } - - void Rename () { renameFile = true; } - - void Delete () { deleteFile = true; } - } - - [Fact (Skip = "Redo for CMv2")] - [AutoInitShutdown] - public void KeyBindings_With_ContextMenu_And_MenuBar () - { - var newFile = false; - var renameFile = false; - - var menuBar = new MenuBar - { - Menus = - [ - new ( - "File", - new MenuItem [] - { - new ("New", string.Empty, New, null, null, Key.N.WithCtrl) - }) - ] - }; - var cm = new ContextMenu (); - - var menuItems = new MenuBarItem ( - [ - new ("Rename File", string.Empty, Rename, null, null, Key.R.WithCtrl), - ] - ); - var top = new Toplevel (); - top.Add (menuBar); - Application.Begin (top); - - Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _)); - Assert.False (menuBar.HotKeyBindings.TryGet (Key.R.WithCtrl, out _)); - Assert.Null (cm.MenuBar); - - Assert.True (Application.RaiseKeyDownEvent (Key.N.WithCtrl)); - Assert.False (Application.RaiseKeyDownEvent (Key.R.WithCtrl)); - Application.MainLoop!.RunIteration (); - Assert.True (newFile); - Assert.False (renameFile); - - newFile = false; - - cm.Show (menuItems); - Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _)); - Assert.False (menuBar.HotKeyBindings.TryGet (Key.R.WithCtrl, out _)); - Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.N.WithCtrl, out _)); - Assert.True (cm.MenuBar.HotKeyBindings.TryGet (Key.R.WithCtrl, out _)); - - Assert.True (cm.MenuBar.IsMenuOpen); - Assert.True (Application.RaiseKeyDownEvent (Key.N.WithCtrl)); - Application.MainLoop!.RunIteration (); - Assert.True (newFile); - Assert.False (cm.MenuBar!.IsMenuOpen); - cm.Show (menuItems); - Assert.True (Application.RaiseKeyDownEvent (Key.R.WithCtrl)); - Application.MainLoop!.RunIteration (); - Assert.True (renameFile); - Assert.False (cm.MenuBar.IsMenuOpen); - - Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _)); - Assert.False (menuBar.HotKeyBindings.TryGet (Key.R.WithCtrl, out _)); - Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _)); - Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.WithCtrl, out _)); - - newFile = false; - renameFile = false; - Assert.True (Application.RaiseKeyDownEvent (Key.N.WithCtrl)); - Assert.False (Application.RaiseKeyDownEvent (Key.R.WithCtrl)); - Application.MainLoop!.RunIteration (); - Assert.True (newFile); - Assert.False (renameFile); - - top.Dispose (); - - void New () { newFile = true; } - - void Rename () { renameFile = true; } - } - - [Fact (Skip = "Redo for CMv2")] - [AutoInitShutdown] - public void KeyBindings_With_Same_Shortcut_ContextMenu_And_MenuBar () - { - var newMenuBar = false; - var newContextMenu = false; - - var menuBar = new MenuBar - { - Menus = - [ - new ( - "File", - new MenuItem [] - { - new ("New", string.Empty, NewMenuBar, null, null, Key.N.WithCtrl) - }) - ] - }; - var cm = new ContextMenu (); - - var menuItems = new MenuBarItem ( - [ - new ("New File", string.Empty, NewContextMenu, null, null, Key.N.WithCtrl), - ] - ); - var top = new Toplevel (); - top.Add (menuBar); - Application.Begin (top); - - Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _)); - Assert.Null (cm.MenuBar); - - Assert.True (Application.RaiseKeyDownEvent (Key.N.WithCtrl)); - Application.MainLoop!.RunIteration (); - Assert.True (newMenuBar); - Assert.False (newContextMenu); - - newMenuBar = false; - - cm.Show (menuItems); - Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _)); - Assert.True (cm.MenuBar!.HotKeyBindings.TryGet (Key.N.WithCtrl, out _)); - - Assert.True (cm.MenuBar.IsMenuOpen); - Assert.True (Application.RaiseKeyDownEvent (Key.N.WithCtrl)); - Application.MainLoop!.RunIteration (); - Assert.False (newMenuBar); - - // The most focused shortcut is executed - Assert.True (newContextMenu); - Assert.False (cm.MenuBar!.IsMenuOpen); - - Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _)); - Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.N.WithCtrl, out _)); - - newMenuBar = false; - newContextMenu = false; - Assert.True (Application.RaiseKeyDownEvent (Key.N.WithCtrl)); - Application.MainLoop!.RunIteration (); - Assert.True (newMenuBar); - Assert.False (newContextMenu); - - top.Dispose (); - - void NewMenuBar () { newMenuBar = true; } - - void NewContextMenu () { newContextMenu = true; } - } - - [Fact (Skip = "Redo for CMv2")] - [AutoInitShutdown] - public void HotKeys_Removed_On_Close_ContextMenu () - { - var newFile = false; - var renameFile = false; - var deleteFile = false; - - var cm = new ContextMenu (); - - var menuItems = new MenuBarItem ( - [ - new ("_New File", string.Empty, New, null, null), - new ("_Rename File", string.Empty, Rename, null, null), - new ("_Delete File", string.Empty, Delete, null, null) - ] - ); - var top = new Toplevel (); - Application.Begin (top); - - Assert.Null (cm.MenuBar); - Assert.False (Application.RaiseKeyDownEvent (Key.N.WithAlt)); - Assert.False (Application.RaiseKeyDownEvent (Key.R.WithAlt)); - Assert.False (Application.RaiseKeyDownEvent (Key.D.WithAlt)); - Assert.False (newFile); - Assert.False (renameFile); - Assert.False (deleteFile); - - cm.Show (menuItems); - Assert.True (cm.MenuBar!.IsMenuOpen); - Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); - Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.N.NoShift, out _)); - Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.WithAlt, out _)); - Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.NoShift, out _)); - Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.D.WithAlt, out _)); - Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.D.NoShift, out _)); - Assert.Equal (2, Application.Top!.SubViews.Count); - View [] menus = Application.Top!.SubViews.Where (v => v is Menu m && m.Host == cm.MenuBar).ToArray (); - Assert.True (menus [0].HotKeyBindings.TryGet (Key.N.WithAlt, out _)); - Assert.True (menus [0].HotKeyBindings.TryGet (Key.N.NoShift, out _)); - Assert.True (menus [0].HotKeyBindings.TryGet (Key.R.WithAlt, out _)); - Assert.True (menus [0].HotKeyBindings.TryGet (Key.R.NoShift, out _)); - Assert.True (menus [0].HotKeyBindings.TryGet (Key.D.WithAlt, out _)); - Assert.True (menus [0].HotKeyBindings.TryGet (Key.D.NoShift, out _)); - - Assert.True (Application.RaiseKeyDownEvent (Key.N.WithAlt)); - Assert.False (cm.MenuBar!.IsMenuOpen); - Application.MainLoop!.RunIteration (); - Assert.True (newFile); - cm.Show (menuItems); - Assert.True (Application.RaiseKeyDownEvent (Key.R.WithAlt)); - Assert.False (cm.MenuBar.IsMenuOpen); - Application.MainLoop!.RunIteration (); - Assert.True (renameFile); - cm.Show (menuItems); - Assert.True (Application.RaiseKeyDownEvent (Key.D.WithAlt)); - Assert.False (cm.MenuBar.IsMenuOpen); - Application.MainLoop!.RunIteration (); - Assert.True (deleteFile); - - Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); - Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.N.NoShift, out _)); - Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.WithAlt, out _)); - Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.NoShift, out _)); - Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.D.WithAlt, out _)); - Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.D.NoShift, out _)); - - newFile = false; - renameFile = false; - deleteFile = false; - Assert.False (Application.RaiseKeyDownEvent (Key.N.WithAlt)); - Assert.False (Application.RaiseKeyDownEvent (Key.R.WithAlt)); - Assert.False (Application.RaiseKeyDownEvent (Key.D.WithAlt)); - Assert.False (newFile); - Assert.False (renameFile); - Assert.False (deleteFile); - - top.Dispose (); - - void New () { newFile = true; } - - void Rename () { renameFile = true; } - - void Delete () { deleteFile = true; } - } - - [Fact (Skip = "Redo for CMv2")] - [AutoInitShutdown] - public void HotKeys_With_ContextMenu_And_MenuBar () - { - var newFile = false; - var renameFile = false; - - var menuBar = new MenuBar - { - Menus = - [ - new ( - "_File", - new MenuItem [] - { - new ("_New", string.Empty, New) - }) - ] - }; - var cm = new ContextMenu (); - - var menuItems = new MenuBarItem ( - [ - new MenuBarItem ( - "_Edit", - new MenuItem [] - { - new ("_Rename File", string.Empty, Rename) - } - ) - ] - ); - var top = new Toplevel (); - top.Add (menuBar); - Application.Begin (top); - - Assert.True (menuBar.HotKeyBindings.TryGet (Key.F.WithAlt, out _)); - Assert.False (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); - Assert.False (menuBar.HotKeyBindings.TryGet (Key.R.WithAlt, out _)); - View [] menus = Application.Top!.SubViews.Where (v => v is Menu m && m.Host == menuBar).ToArray (); - Assert.Empty (menus); - Assert.Null (cm.MenuBar); - - Assert.True (Application.RaiseKeyDownEvent (Key.F.WithAlt)); - Assert.True (menuBar.IsMenuOpen); - Assert.Equal (2, Application.Top!.SubViews.Count); - menus = Application.Top!.SubViews.Where (v => v is Menu m && m.Host == menuBar).ToArray (); - Assert.True (menus [0].HotKeyBindings.TryGet (Key.N.WithAlt, out _)); - Assert.True (Application.RaiseKeyDownEvent (Key.N.WithAlt)); - Assert.False (menuBar.IsMenuOpen); - Assert.False (Application.RaiseKeyDownEvent (Key.R.WithAlt)); - Application.MainLoop!.RunIteration (); - Assert.True (newFile); - Assert.False (renameFile); - - newFile = false; - - cm.Show (menuItems); - Assert.True (menuBar.HotKeyBindings.TryGet (Key.F.WithAlt, out _)); - Assert.True (menuBar.HotKeyBindings.TryGet (Key.F.NoShift, out _)); - Assert.False (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); - Assert.False (menuBar.HotKeyBindings.TryGet (Key.N.NoShift, out _)); - Assert.False (menuBar.HotKeyBindings.TryGet (Key.E.WithAlt, out _)); - Assert.False (menuBar.HotKeyBindings.TryGet (Key.E.NoShift, out _)); - Assert.False (menuBar.HotKeyBindings.TryGet (Key.R.WithAlt, out _)); - Assert.False (menuBar.HotKeyBindings.TryGet (Key.R.NoShift, out _)); - Assert.True (cm.MenuBar!.IsMenuOpen); - Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.F.WithAlt, out _)); - Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.F.NoShift, out _)); - Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); - Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.N.NoShift, out _)); - Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.E.WithAlt, out _)); - Assert.False (cm.MenuBar!.HotKeyBindings.TryGet (Key.E.NoShift, out _)); - Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.WithAlt, out _)); - Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.NoShift, out _)); - Assert.Equal (4, Application.Top!.SubViews.Count); - menus = Application.Top!.SubViews.Where (v => v is Menu m && m.Host == cm.MenuBar).ToArray (); - Assert.True (menus [0].HotKeyBindings.TryGet (Key.E.WithAlt, out _)); - Assert.True (menus [0].HotKeyBindings.TryGet (Key.E.NoShift, out _)); - Assert.True (menus [1].HotKeyBindings.TryGet (Key.R.WithAlt, out _)); - Assert.True (menus [1].HotKeyBindings.TryGet (Key.R.NoShift, out _)); - Assert.True (cm.MenuBar.IsMenuOpen); - Assert.True (Application.RaiseKeyDownEvent (Key.F.WithAlt)); - Assert.False (cm.MenuBar.IsMenuOpen); - Assert.True (Application.RaiseKeyDownEvent (Key.N.WithAlt)); - Application.MainLoop!.RunIteration (); - Assert.True (newFile); - - cm.Show (menuItems); - Assert.True (cm.MenuBar.IsMenuOpen); - Assert.Equal (4, Application.Top!.SubViews.Count); - menus = Application.Top!.SubViews.Where (v => v is Menu m && m.Host == cm.MenuBar).ToArray (); - Assert.True (menus [0].HotKeyBindings.TryGet (Key.E.WithAlt, out _)); - Assert.True (menus [0].HotKeyBindings.TryGet (Key.E.NoShift, out _)); - Assert.False (menus [0].HotKeyBindings.TryGet (Key.R.WithAlt, out _)); - Assert.False (menus [0].HotKeyBindings.TryGet (Key.R.NoShift, out _)); - Assert.False (menus [1].HotKeyBindings.TryGet (Key.E.WithAlt, out _)); - Assert.False (menus [1].HotKeyBindings.TryGet (Key.E.NoShift, out _)); - Assert.True (menus [1].HotKeyBindings.TryGet (Key.R.WithAlt, out _)); - Assert.True (menus [1].HotKeyBindings.TryGet (Key.R.NoShift, out _)); - Assert.True (Application.RaiseKeyDownEvent (Key.E.NoShift)); - Assert.True (Application.RaiseKeyDownEvent (Key.R.WithAlt)); - Assert.False (cm.MenuBar.IsMenuOpen); - Application.MainLoop!.RunIteration (); - Assert.True (renameFile); - - Assert.Equal (2, Application.Top!.SubViews.Count); - Assert.True (menuBar.HotKeyBindings.TryGet (Key.F.WithAlt, out _)); - Assert.True (menuBar.HotKeyBindings.TryGet (Key.F.NoShift, out _)); - Assert.False (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); - Assert.False (menuBar.HotKeyBindings.TryGet (Key.N.NoShift, out _)); - Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.E.WithAlt, out _)); - Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.E.NoShift, out _)); - Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.WithAlt, out _)); - Assert.False (cm.MenuBar.HotKeyBindings.TryGet (Key.R.NoShift, out _)); - - newFile = false; - renameFile = false; - Assert.True (Application.RaiseKeyDownEvent (Key.F.WithAlt)); - Assert.True (Application.RaiseKeyDownEvent (Key.N.WithAlt)); - Assert.False (Application.RaiseKeyDownEvent (Key.R.WithAlt)); - Application.MainLoop!.RunIteration (); - Assert.True (newFile); - Assert.False (renameFile); - - top.Dispose (); - - void New () { newFile = true; } - - void Rename () { renameFile = true; } - } - - [Fact (Skip = "Redo for CMv2")] - [AutoInitShutdown] - public void Opened_MenuBar_Is_Closed_When_Another_MenuBar_Is_Opening_Also_By_HotKey () - { - var menuBar = new MenuBar - { - Menus = - [ - new ( - "_File", - new MenuItem [] - { - new ("_New", string.Empty, null) - }) - ] - }; - var cm = new ContextMenu (); - - var menuItems = new MenuBarItem ( - [ - new MenuBarItem ( - "_Edit", - new MenuItem [] - { - new ("_Rename File", string.Empty, null) - } - ) - ] - ); - var top = new Toplevel (); - top.Add (menuBar); - Application.Begin (top); - - Assert.True (Application.RaiseKeyDownEvent (Key.F.WithAlt)); - Assert.True (menuBar.IsMenuOpen); - - cm.Show (menuItems); - Assert.False (menuBar.IsMenuOpen); - Assert.True (cm.MenuBar!.IsMenuOpen); - - Assert.True (Application.RaiseKeyDownEvent (Key.F.WithAlt)); - Assert.True (menuBar.IsMenuOpen); - Assert.False (cm.MenuBar!.IsMenuOpen); - - top.Dispose (); - } - - [Theory] - [InlineData (1)] - [InlineData (2)] - [InlineData (3)] - [AutoInitShutdown] - public void Mouse_Pressed_Released_Clicked (int button) - { - var actionRaised = false; - - var menuBar = new MenuBar - { - Menus = - [ - new ( - "_File", - new MenuItem [] - { - new ("_New", string.Empty, () => actionRaised = true) - }) - ] - }; - var cm = new ContextMenu (); - - var menuItems = new MenuBarItem ( - [ - new ("_Rename File", string.Empty, () => actionRaised = true) - ] - ); - var top = new Toplevel (); - - top.MouseClick += (s, e) => - { - if (e.Flags == cm.MouseFlags) - { - cm.Position = new (e.Position.X, e.Position.Y); - cm.Show (menuItems); - e.Handled = true; - } - }; - - top.Add (menuBar); - Application.Begin (top); - - // MenuBar - Application.RaiseMouseEvent (new () { Flags = MouseFlags.Button1Pressed }); - Assert.True (menuBar.IsMenuOpen); - - switch (button) - { - // Left Button - case 1: - Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 2), Flags = MouseFlags.Button1Pressed }); - Assert.True (menuBar.IsMenuOpen); - Application.MainLoop.RunIteration (); - Assert.False (actionRaised); - Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 2), Flags = MouseFlags.Button1Released }); - Assert.True (menuBar.IsMenuOpen); - Application.MainLoop.RunIteration (); - Assert.False (actionRaised); - Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 2), Flags = MouseFlags.Button1Clicked }); - Assert.False (menuBar.IsMenuOpen); - Application.MainLoop.RunIteration (); - Assert.True (actionRaised); - actionRaised = false; - - break; - // Middle Button - case 2: - // Right Button - case 3: - Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 2), Flags = MouseFlags.Button3Pressed }); - Assert.True (menuBar.IsMenuOpen); - Application.MainLoop.RunIteration (); - Assert.False (actionRaised); - Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 2), Flags = MouseFlags.Button3Released }); - Assert.True (menuBar.IsMenuOpen); - Application.MainLoop.RunIteration (); - Assert.False (actionRaised); - Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 2), Flags = MouseFlags.Button3Clicked }); - Assert.True (menuBar.IsMenuOpen); - Application.MainLoop.RunIteration (); - Assert.False (actionRaised); - - break; - } - - // ContextMenu - Application.RaiseMouseEvent (new () { ScreenPosition = new (0, 4), Flags = cm.MouseFlags }); - Assert.False (menuBar.IsMenuOpen); - Assert.True (cm.MenuBar!.IsMenuOpen); - - switch (button) - { - // Left Button - case 1: - Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 6), Flags = MouseFlags.Button1Pressed }); - Assert.True (cm.MenuBar!.IsMenuOpen); - Application.MainLoop.RunIteration (); - Assert.False (actionRaised); - Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 6), Flags = MouseFlags.Button1Released }); - Assert.True (cm.MenuBar!.IsMenuOpen); - Application.MainLoop.RunIteration (); - Assert.False (actionRaised); - Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 6), Flags = MouseFlags.Button1Clicked }); - Assert.False (cm.MenuBar!.IsMenuOpen); - Application.MainLoop.RunIteration (); - Assert.True (actionRaised); - actionRaised = false; - - break; - // Middle Button - case 2: - Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 4), Flags = MouseFlags.Button2Pressed }); - Assert.False (cm.MenuBar!.IsMenuOpen); - Application.MainLoop.RunIteration (); - Assert.False (actionRaised); - Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 4), Flags = MouseFlags.Button2Released }); - Assert.False (cm.MenuBar!.IsMenuOpen); - Application.MainLoop.RunIteration (); - Assert.False (actionRaised); - Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 4), Flags = MouseFlags.Button2Clicked }); - Assert.False (cm.MenuBar!.IsMenuOpen); - Application.MainLoop.RunIteration (); - Assert.False (actionRaised); - - break; - // Right Button - case 3: - Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 4), Flags = MouseFlags.Button3Pressed }); - Assert.False (cm.MenuBar!.IsMenuOpen); - Application.MainLoop.RunIteration (); - Assert.False (actionRaised); - Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 4), Flags = MouseFlags.Button3Released }); - Assert.False (cm.MenuBar!.IsMenuOpen); - Application.MainLoop.RunIteration (); - Assert.False (actionRaised); - Application.RaiseMouseEvent (new () { ScreenPosition = new (1, 4), Flags = MouseFlags.Button3Clicked }); - // MouseFlags is the same as cm.MouseFlags. So the context menu is closed and reopened again - Assert.True (cm.MenuBar!.IsMenuOpen); - Application.MainLoop.RunIteration (); - Assert.False (actionRaised); - - break; - } - - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void Menu_Without_SubMenu_Is_Closed_When_Pressing_Key_Right_Or_Key_Left () - { - var cm = new ContextMenu (); - - var menuItems = new MenuBarItem ( - [ - new ("_New", string.Empty, null), - new ("_Save", string.Empty, null) - ] - ); - var top = new Toplevel (); - Application.Begin (top); - - cm.Show (menuItems); - Assert.True (cm.MenuBar!.IsMenuOpen); - - Assert.True (Application.RaiseKeyDownEvent (Key.CursorRight)); - Assert.False (cm.MenuBar!.IsMenuOpen); - - cm.Show (menuItems); - Assert.True (cm.MenuBar!.IsMenuOpen); - - Assert.True (Application.RaiseKeyDownEvent (Key.CursorLeft)); - Assert.False (cm.MenuBar!.IsMenuOpen); - - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void Menu_Opened_In_SuperView_With_TabView_Has_Precedence_On_Key_Press () - { - var win = new Window - { - Title = "My Window", - X = 0, - Y = 0, - Width = Dim.Fill (), - Height = Dim.Fill () - }; - - // Tab View - var tabView = new TabView - { - X = 1, - Y = 1, - Width = Dim.Fill () - 2, - Height = Dim.Fill () - 2 - }; - tabView.AddTab (new () { DisplayText = "Tab 1" }, true); - tabView.AddTab (new () { DisplayText = "Tab 2" }, false); - win.Add (tabView); - - // Context Menu - var menuItems = new MenuBarItem ( - [ - new ("Item 1", "First item", () => MessageBox.Query ("Action", "Item 1 Clicked", "OK")), - new MenuBarItem ( - "Submenu", - new List - { - new [] - { - new MenuItem ( - "Sub Item 1", - "Submenu item", - () => { MessageBox.Query ("Action", "Sub Item 1 Clicked", "OK"); }) - } - }) - ]); - - var cm = new ContextMenu (); - - win.MouseClick += (s, e) => - { - if (e.Flags.HasFlag (MouseFlags.Button3Clicked)) // Right-click - { - cm.Position = e.Position; - cm.Show (menuItems); - } - }; - Application.Begin (win); - - cm.Show (menuItems); - Assert.True (cm.MenuBar!.IsMenuOpen); - - Assert.True (Application.RaiseKeyDownEvent (Key.CursorDown)); - Assert.True (cm.MenuBar!.IsMenuOpen); - - Assert.True (Application.RaiseKeyDownEvent (Key.CursorUp)); - Assert.True (cm.MenuBar!.IsMenuOpen); - - Assert.True (Application.RaiseKeyDownEvent (Key.CursorDown)); - Assert.True (cm.MenuBar!.IsMenuOpen); - - Assert.True (Application.RaiseKeyDownEvent (Key.CursorRight)); - Assert.True (cm.MenuBar!.IsMenuOpen); - - Assert.True (Application.RaiseKeyDownEvent (Key.CursorLeft)); - Assert.True (cm.MenuBar!.IsMenuOpen); - - Assert.True (Application.RaiseKeyDownEvent (Key.CursorLeft)); - Assert.False (cm.MenuBar!.IsMenuOpen); - Assert.True (tabView.HasFocus); - - win.Dispose (); - } -} diff --git a/Tests/UnitTests/Views/TextViewTests.cs b/Tests/UnitTests/Views/TextViewTests.cs index 02de26bfb4..de4db0a049 100644 --- a/Tests/UnitTests/Views/TextViewTests.cs +++ b/Tests/UnitTests/Views/TextViewTests.cs @@ -5533,7 +5533,7 @@ public void KeyBindings_Command () Assert.False (tv.NewKeyDownEvent (Key.F6.WithShift)); Assert.False (tv.NewKeyDownEvent (Application.PrevTabGroupKey)); - Assert.True (tv.NewKeyDownEvent (ContextMenu.DefaultKey)); + Assert.True (tv.NewKeyDownEvent (PopoverMenu.DefaultKey)); Assert.True (tv.ContextMenu != null && tv.ContextMenu.Visible); top.Dispose (); } diff --git a/Tests/UnitTestsParallelizable/Application/ApplicationPopoverTests.cs b/Tests/UnitTestsParallelizable/Application/ApplicationPopoverTests.cs index 060c26f580..17e9d33212 100644 --- a/Tests/UnitTestsParallelizable/Application/ApplicationPopoverTests.cs +++ b/Tests/UnitTestsParallelizable/Application/ApplicationPopoverTests.cs @@ -35,29 +35,29 @@ public void DeRegister_RemovesPopover () } [Fact] - public void ShowPopover_SetsActivePopover () + public void Show_SetsActivePopover () { // Arrange var popover = new Mock ().Object; var popoverManager = new ApplicationPopover (); // Act - popoverManager.ShowPopover (popover); + popoverManager.Show (popover); // Assert Assert.Equal (popover, popoverManager.GetActivePopover ()); } [Fact] - public void HidePopover_ClearsActivePopover () + public void Hide_ClearsActivePopover () { // Arrange var popover = new Mock ().Object; var popoverManager = new ApplicationPopover (); - popoverManager.ShowPopover (popover); + popoverManager.Show (popover); // Act - popoverManager.HidePopover (popover); + popoverManager.Hide (popover); // Assert Assert.Null (popoverManager.GetActivePopover ()); @@ -70,7 +70,7 @@ public void DispatchKeyDown_ActivePopoverGetsKey () // Arrange var popover = new IPopoverTestClass (); var popoverManager = new ApplicationPopover (); - popoverManager.ShowPopover (popover); + popoverManager.Show (popover); // Act popoverManager.DispatchKeyDown (Key.A); @@ -86,7 +86,7 @@ public void DispatchKeyDown_ActivePopoverGetsHotKey () // Arrange var popover = new IPopoverTestClass (); var popoverManager = new ApplicationPopover (); - popoverManager.ShowPopover (popover); + popoverManager.Show (popover); // Act popoverManager.DispatchKeyDown (Key.N.WithCtrl); @@ -104,7 +104,7 @@ public void DispatchKeyDown_InactivePopoverGetsHotKey () var activePopover = new IPopoverTestClass () { Id = "activePopover" }; var inactivePopover = new IPopoverTestClass () { Id = "inactivePopover" }; ; var popoverManager = new ApplicationPopover (); - popoverManager.ShowPopover (activePopover); + popoverManager.Show (activePopover); popoverManager.Register (inactivePopover); // Act @@ -124,7 +124,7 @@ public void DispatchKeyDown_InactivePopoverDoesGetKey () var activePopover = new IPopoverTestClass (); var inactivePopover = new IPopoverTestClass (); var popoverManager = new ApplicationPopover (); - popoverManager.ShowPopover (activePopover); + popoverManager.Show (activePopover); popoverManager.Register (inactivePopover); // Act diff --git a/UICatalog/Scenarios/ContextMenus.cs b/UICatalog/Scenarios/ContextMenus.cs index 0d9d50e38b..0f0d242420 100644 --- a/UICatalog/Scenarios/ContextMenus.cs +++ b/UICatalog/Scenarios/ContextMenus.cs @@ -9,7 +9,7 @@ namespace UICatalog.Scenarios; public class ContextMenus : Scenario { [CanBeNull] - private ContextMenuv2 _winContextMenu; + private PopoverMenu _winContextMenu; private TextField _tfTopLeft, _tfTopRight, _tfMiddle, _tfBottomLeft, _tfBottomRight; private readonly List _cultureInfos = Application.SupportedCultures; private readonly Key _winContextMenuKey = Key.Space.WithCtrl; @@ -42,7 +42,7 @@ public override void Main () { X = Pos.Center (), Y = Pos.Bottom (label), - Text = $"Press '{ContextMenu.DefaultKey}' to open the TextField context menu." + Text = $"Press '{PopoverMenu.DefaultKey}' to open the TextField context menu." }; appWindow.Add (label); diff --git a/UICatalog/Scenarios/MenusV2.cs b/UICatalog/Scenarios/MenusV2.cs index 756efd9dfc..9239f527b6 100644 --- a/UICatalog/Scenarios/MenusV2.cs +++ b/UICatalog/Scenarios/MenusV2.cs @@ -121,8 +121,6 @@ public TargetView () KeyBindings.Add (PopoverMenu.DefaultKey, Command.Context); - MouseBindings.ReplaceCommands (PopoverMenu.MouseFlags, Command.Context); - AddCommand ( Command.Cancel, ctx => @@ -504,22 +502,6 @@ private void ConfigureMoreDetailsSubMenu (Menuv2 menu) menu.Add (deeperDetail, new Line (), shortcut4); } - - /// - protected override void Dispose (bool disposing) - { - if (disposing) - { - // if (FilePopoverMenu is { }) - // { - // FilePopoverMenu.Visible = false; - // FilePopoverMenu?.Dispose (); - // FilePopoverMenu = null; - // } - } - - base.Dispose (disposing); - } } private const string LOGFILE_LOCATION = "./logs"; diff --git a/UICatalog/Scenarios/Notepad.cs b/UICatalog/Scenarios/Notepad.cs index 1a0871d557..3c079d27e2 100644 --- a/UICatalog/Scenarios/Notepad.cs +++ b/UICatalog/Scenarios/Notepad.cs @@ -333,39 +333,37 @@ private void TabView_TabClicked (object sender, TabMouseEventArgs e) return; } - MenuBarItem items; + View [] items; if (e.Tab == null) { - items = new ( - new MenuItem [] { new ("Open", "", () => Open ()) } - ); + items = [new MenuItemv2 ("Open", "", Open)]; } else { var tv = (TabView)sender; var t = (OpenedFile)e.Tab; - items = new ( - new MenuItem [] - { - new ("Save", "", () => Save (_focusedTabView, e.Tab)), - new ("Close", "", () => Close (tv, e.Tab)), - null, - new ("Split Up", "", () => SplitUp (tv, t)), - new ("Split Down", "", () => SplitDown (tv, t)), - new ("Split Right", "", () => SplitRight (tv, t)), - new ("Split Left", "", () => SplitLeft (tv, t)) - } - ); + items = + [ + new MenuItemv2 ("Save", "", () => Save (_focusedTabView, e.Tab)), + new MenuItemv2 ("Close", "", () => Close (tv, e.Tab)), + new Line (), + new MenuItemv2 ("Split Up", "", () => SplitUp (tv, t)), + new MenuItemv2 ("Split Down", "", () => SplitDown (tv, t)), + new MenuItemv2 ("Split Right", "", () => SplitRight (tv, t)), + new MenuItemv2 ("Split Left", "", () => SplitLeft (tv, t)) + ]; + + PopoverMenu? contextMenu = new (items); + + // Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused + // and the context menu is disposed when it is closed. + Application.Popover?.Register (contextMenu); + contextMenu?.MakeVisible (e.MouseEvent.ScreenPosition); + + e.MouseEvent.Handled = true; } - - var screen = ((View)sender).ViewportToScreen (e.MouseEvent.Position); - - var contextMenu = new ContextMenu { Position = screen }; - - contextMenu.Show (items); - e.MouseEvent.Handled = true; } private class OpenedFile (Notepad notepad) : Tab diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index a85d30ecc7..20b459b93e 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Data; +using System.Data; using System.Globalization; -using System.IO; -using System.Linq; using System.Text; using Terminal.Gui; @@ -827,10 +823,10 @@ private void EditCurrentCell (object sender, CellActivatedEventArgs e) var ok = new Button { Text = "Ok", IsDefault = true }; ok.Accepting += (s, e) => - { - okPressed = true; - Application.RequestStop (); - }; + { + okPressed = true; + Application.RequestStop (); + }; var cancel = new Button { Text = "Cancel" }; cancel.Accepting += (s, e) => { Application.RequestStop (); }; var d = new Dialog { Title = title, Buttons = [ok, cancel] }; @@ -1025,12 +1021,13 @@ Func getter var ok = new Button { Text = "Ok", IsDefault = true }; ok.Accepting += (s, e) => - { - accepted = true; - Application.RequestStop (); - }; + { + accepted = true; + Application.RequestStop (); + }; var cancel = new Button { Text = "Cancel" }; cancel.Accepting += (s, e) => { Application.RequestStop (); }; + var d = new Dialog { Title = prompt, @@ -1212,30 +1209,24 @@ private void ShowHeaderContextMenu (int clickedCol, MouseEventArgs e) string sort = GetProposedNewSortOrder (clickedCol, out bool isAsc); string colName = _tableView.Table.ColumnNames [clickedCol]; - var contextMenu = new ContextMenu - { - Position = new (e.Position.X + 1, e.Position.Y + 1) - }; + PopoverMenu? contextMenu = new ( + [ + new ( + $"Hide {TrimArrows (colName)}", + "", + () => HideColumn (clickedCol) + ), + new ( + $"Sort {StripArrows (sort)}", + "", + () => SortColumn (clickedCol, sort, isAsc) + ) + ]); - MenuBarItem menuItems = new ( - [ - new ( - $"Hide {TrimArrows (colName)}", - "", - () => HideColumn (clickedCol) - ), - new ( - $"Sort {StripArrows (sort)}", - "", - () => SortColumn ( - clickedCol, - sort, - isAsc - ) - ) - ] - ); - contextMenu.Show (menuItems); + // Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused + // and the context menu is disposed when it is closed. + Application.Popover?.Register (contextMenu); + contextMenu?.MakeVisible (new (e.ScreenPosition.X + 1, e.ScreenPosition.Y + 1)); } private void SortColumn (int clickedCol) @@ -1413,7 +1404,7 @@ private void ToggleCheckboxes (bool radio) _checkedFileSystemInfos.Contains, CheckOrUncheckFile ) - { UseRadioButtons = radio }; + { UseRadioButtons = radio }; } else { diff --git a/UICatalog/Scenarios/TreeViewFileSystem.cs b/UICatalog/Scenarios/TreeViewFileSystem.cs index f371a18f84..b194d95179 100644 --- a/UICatalog/Scenarios/TreeViewFileSystem.cs +++ b/UICatalog/Scenarios/TreeViewFileSystem.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.IO; using System.IO.Abstractions; -using System.Linq; using System.Text; using Terminal.Gui; @@ -37,6 +34,7 @@ public class TreeViewFileSystem : Scenario public override void Main () { Application.Init (); + var win = new Window { Title = GetName (), @@ -49,142 +47,142 @@ public override void Main () { Menus = [ - new MenuBarItem ( - "_File", - new MenuItem [] - { - new ( - "_Quit", - $"{Application.QuitKey}", - () => Quit () - ) - } - ), - new MenuBarItem ( - "_View", - new [] - { - _miFullPaths = - new MenuItem ("_Full Paths", "", () => SetFullName ()) - { - Checked = false, CheckType = MenuItemCheckStyle.Checked - }, - _miMultiSelect = new MenuItem ( - "_Multi Select", - "", - () => SetMultiSelect () - ) - { - Checked = true, - CheckType = MenuItemCheckStyle - .Checked - } - } - ), - new MenuBarItem ( - "_Style", - new [] - { - _miShowLines = - new MenuItem ("_Show Lines", "", () => ShowLines ()) - { - Checked = true, CheckType = MenuItemCheckStyle.Checked - }, - null /*separator*/, - _miPlusMinus = - new MenuItem ( - "_Plus Minus Symbols", - "+ -", - () => SetExpandableSymbols ( - (Rune)'+', - (Rune)'-' - ) - ) { Checked = true, CheckType = MenuItemCheckStyle.Radio }, - _miArrowSymbols = - new MenuItem ( - "_Arrow Symbols", - "> v", - () => SetExpandableSymbols ( - (Rune)'>', - (Rune)'v' - ) - ) { Checked = false, CheckType = MenuItemCheckStyle.Radio }, - _miNoSymbols = - new MenuItem ( - "_No Symbols", - "", - () => SetExpandableSymbols ( - default (Rune), - null - ) - ) { Checked = false, CheckType = MenuItemCheckStyle.Radio }, - null /*separator*/, - _miColoredSymbols = - new MenuItem ( - "_Colored Symbols", - "", - () => ShowColoredExpandableSymbols () - ) { Checked = false, CheckType = MenuItemCheckStyle.Checked }, - _miInvertSymbols = - new MenuItem ( - "_Invert Symbols", - "", - () => InvertExpandableSymbols () - ) { Checked = false, CheckType = MenuItemCheckStyle.Checked }, - null /*separator*/, - _miBasicIcons = - new MenuItem ("_Basic Icons", null, SetNoIcons) - { - Checked = false, CheckType = MenuItemCheckStyle.Radio - }, - _miUnicodeIcons = - new MenuItem ("_Unicode Icons", null, SetUnicodeIcons) - { - Checked = false, CheckType = MenuItemCheckStyle.Radio - }, - _miNerdIcons = - new MenuItem ("_Nerd Icons", null, SetNerdIcons) - { - Checked = false, CheckType = MenuItemCheckStyle.Radio - }, - null /*separator*/, - _miLeaveLastRow = - new MenuItem ( - "_Leave Last Row", - "", - () => SetLeaveLastRow () - ) { Checked = true, CheckType = MenuItemCheckStyle.Checked }, - _miHighlightModelTextOnly = - new MenuItem ( - "_Highlight Model Text Only", - "", - () => SetCheckHighlightModelTextOnly () - ) { Checked = false, CheckType = MenuItemCheckStyle.Checked }, - null /*separator*/, - _miCustomColors = - new MenuItem ( - "C_ustom Colors Hidden Files", - "Yellow/Red", - () => SetCustomColors () - ) { Checked = false, CheckType = MenuItemCheckStyle.Checked }, - null /*separator*/, - _miCursor = new MenuItem ( - "Curs_or (MultiSelect only)", - "", - () => SetCursor () - ) { Checked = false, CheckType = MenuItemCheckStyle.Checked } - } - ) + new ( + "_File", + new MenuItem [] + { + new ( + "_Quit", + $"{Application.QuitKey}", + () => Quit () + ) + } + ), + new ( + "_View", + new [] + { + _miFullPaths = + new ("_Full Paths", "", () => SetFullName ()) + { + Checked = false, CheckType = MenuItemCheckStyle.Checked + }, + _miMultiSelect = new ( + "_Multi Select", + "", + () => SetMultiSelect () + ) + { + Checked = true, + CheckType = MenuItemCheckStyle + .Checked + } + } + ), + new ( + "_Style", + new [] + { + _miShowLines = + new ("_Show Lines", "", () => ShowLines ()) + { + Checked = true, CheckType = MenuItemCheckStyle.Checked + }, + null /*separator*/, + _miPlusMinus = + new ( + "_Plus Minus Symbols", + "+ -", + () => SetExpandableSymbols ( + (Rune)'+', + (Rune)'-' + ) + ) { Checked = true, CheckType = MenuItemCheckStyle.Radio }, + _miArrowSymbols = + new ( + "_Arrow Symbols", + "> v", + () => SetExpandableSymbols ( + (Rune)'>', + (Rune)'v' + ) + ) { Checked = false, CheckType = MenuItemCheckStyle.Radio }, + _miNoSymbols = + new ( + "_No Symbols", + "", + () => SetExpandableSymbols ( + default (Rune), + null + ) + ) { Checked = false, CheckType = MenuItemCheckStyle.Radio }, + null /*separator*/, + _miColoredSymbols = + new ( + "_Colored Symbols", + "", + () => ShowColoredExpandableSymbols () + ) { Checked = false, CheckType = MenuItemCheckStyle.Checked }, + _miInvertSymbols = + new ( + "_Invert Symbols", + "", + () => InvertExpandableSymbols () + ) { Checked = false, CheckType = MenuItemCheckStyle.Checked }, + null /*separator*/, + _miBasicIcons = + new ("_Basic Icons", null, SetNoIcons) + { + Checked = false, CheckType = MenuItemCheckStyle.Radio + }, + _miUnicodeIcons = + new ("_Unicode Icons", null, SetUnicodeIcons) + { + Checked = false, CheckType = MenuItemCheckStyle.Radio + }, + _miNerdIcons = + new ("_Nerd Icons", null, SetNerdIcons) + { + Checked = false, CheckType = MenuItemCheckStyle.Radio + }, + null /*separator*/, + _miLeaveLastRow = + new ( + "_Leave Last Row", + "", + () => SetLeaveLastRow () + ) { Checked = true, CheckType = MenuItemCheckStyle.Checked }, + _miHighlightModelTextOnly = + new ( + "_Highlight Model Text Only", + "", + () => SetCheckHighlightModelTextOnly () + ) { Checked = false, CheckType = MenuItemCheckStyle.Checked }, + null /*separator*/, + _miCustomColors = + new ( + "C_ustom Colors Hidden Files", + "Yellow/Red", + () => SetCustomColors () + ) { Checked = false, CheckType = MenuItemCheckStyle.Checked }, + null /*separator*/, + _miCursor = new ( + "Curs_or (MultiSelect only)", + "", + () => SetCursor () + ) { Checked = false, CheckType = MenuItemCheckStyle.Checked } + } + ) ] }; top.Add (menu); - _treeViewFiles = new TreeView { X = 0, Y = 0, Width = Dim.Percent (50), Height = Dim.Fill () }; + _treeViewFiles = new() { X = 0, Y = 0, Width = Dim.Percent (50), Height = Dim.Fill () }; _treeViewFiles.DrawLine += TreeViewFiles_DrawLine; _treeViewFiles.VerticalScrollBar.AutoShow = false; - _detailsFrame = new DetailsFrame (_iconProvider) + _detailsFrame = new (_iconProvider) { X = Pos.Right (_treeViewFiles), Y = 0, Width = Dim.Fill (), Height = Dim.Fill () }; @@ -249,16 +247,16 @@ private void SetCustomColors () { if (m is IDirectoryInfo && m.Attributes.HasFlag (FileAttributes.Hidden)) { - return new ColorScheme + return new() { - Focus = new Attribute ( - Color.BrightRed, - _treeViewFiles.ColorScheme.Focus.Background - ), - Normal = new Attribute ( - Color.BrightYellow, - _treeViewFiles.ColorScheme.Normal.Background - ) + Focus = new ( + Color.BrightRed, + _treeViewFiles.ColorScheme.Focus.Background + ), + Normal = new ( + Color.BrightYellow, + _treeViewFiles.ColorScheme.Normal.Background + ) }; ; @@ -266,16 +264,16 @@ private void SetCustomColors () if (m is IFileInfo && m.Attributes.HasFlag (FileAttributes.Hidden)) { - return new ColorScheme + return new() { - Focus = new Attribute ( - Color.BrightRed, - _treeViewFiles.ColorScheme.Focus.Background - ), - Normal = new Attribute ( - Color.BrightYellow, - _treeViewFiles.ColorScheme.Normal.Background - ) + Focus = new ( + Color.BrightRed, + _treeViewFiles.ColorScheme.Focus.Background + ), + Normal = new ( + Color.BrightYellow, + _treeViewFiles.ColorScheme.Normal.Background + ) }; ; @@ -417,13 +415,13 @@ private void ShowColoredExpandableSymbols () private void ShowContextMenu (Point screenPoint, IFileSystemInfo forObject) { - var menu = new ContextMenu { Position = screenPoint }; + PopoverMenu? contextMenu = new ([new ("Properties", $"Show {forObject.Name} properties", () => ShowPropertiesOf (forObject))]); - var menuItems = new MenuBarItem ( - new [] { new MenuItem ("Properties", null, () => ShowPropertiesOf (forObject)) } - ); + // Registering with the PopoverManager will ensure that the context menu is closed when the view is no longer focused + // and the context menu is disposed when it is closed. + Application.Popover?.Register (contextMenu); - Application.Invoke (() => menu.Show (menuItems)); + Application.Invoke (() => contextMenu?.MakeVisible (screenPoint)); } private void ShowLines () @@ -477,10 +475,10 @@ private void TreeViewFiles_KeyPress (object sender, Key obj) } ShowContextMenu ( - new Point ( - 5 + _treeViewFiles.Frame.X, - location.Value + _treeViewFiles.Frame.Y + 2 - ), + new ( + 5 + _treeViewFiles.Frame.X, + location.Value + _treeViewFiles.Frame.Y + 2 + ), selected ); } @@ -500,10 +498,10 @@ private void TreeViewFiles_MouseClick (object sender, MouseEventArgs obj) } ShowContextMenu ( - new Point ( - obj.Position.X + _treeViewFiles.Frame.X, - obj.Position.Y + _treeViewFiles.Frame.Y + 2 - ), + new ( + obj.Position.X + _treeViewFiles.Frame.X, + obj.Position.Y + _treeViewFiles.Frame.Y + 2 + ), rightClicked ); } @@ -543,7 +541,7 @@ public IFileSystemInfo FileInfo if (_fileInfo is IFileInfo f) { Title = $"{_iconProvider.GetIconWithOptionalSpace (f)}{f.Name}".Trim (); - sb = new StringBuilder (); + sb = new (); sb.AppendLine ($"Path:\n {f.FullName}\n"); sb.AppendLine ($"Size:\n {f.Length:N0} bytes\n"); sb.AppendLine ($"Modified:\n {f.LastWriteTime}\n"); @@ -553,7 +551,7 @@ public IFileSystemInfo FileInfo if (_fileInfo is IDirectoryInfo dir) { Title = $"{_iconProvider.GetIconWithOptionalSpace (dir)}{dir.Name}".Trim (); - sb = new StringBuilder (); + sb = new (); sb.AppendLine ($"Path:\n {dir?.FullName}\n"); sb.AppendLine ($"Modified:\n {dir.LastWriteTime}\n"); sb.AppendLine ($"Created:\n {dir.CreationTime}\n"); diff --git a/docfx/docs/migratingfromv1.md b/docfx/docs/migratingfromv1.md index 56506e584b..374d1e9f09 100644 --- a/docfx/docs/migratingfromv1.md +++ b/docfx/docs/migratingfromv1.md @@ -470,6 +470,24 @@ In v1, only Views derived from `Toplevel` could be overlapped. In v2, any view c v1 conflated the concepts of +## `ContextMenu` replaced by `PopoverMenu` + +`PopoverMenu` replaces `ContrextMenu`. + +## `MenuItem` is now based on `Shortcut` + + +```diff +new ( + Strings.charMapCopyGlyph, + "", + CopyGlyph, +- null, +- null, + (KeyCode)Key.G.WithCtrl + ), +``` + ## Others... * `View` and all subclasses support `IDisposable` and must be disposed (by calling `view.Dispose ()`) by whatever code owns the instance when the instance is longer needed. From 47833bfc809aae5c881b8c433765a96542182b7f Mon Sep 17 00:00:00 2001 From: Tig Date: Wed, 2 Apr 2025 06:27:24 -0600 Subject: [PATCH 05/28] Partially Fixes #2975 - Upgrades UICatalog to `Menuv2` (#4011) --- .../Application/ApplicationPopover.cs | 2 +- Terminal.Gui/View/View.Command.cs | 8 +- Terminal.Gui/View/View.Keyboard.cs | 10 + Terminal.Gui/Views/FlagSelector.cs | 367 ++ Terminal.Gui/Views/FlagSelectorStyles.cs | 31 + Terminal.Gui/Views/Menu/MenuBarItemv2.cs | 30 +- Terminal.Gui/Views/Menu/MenuBarv2.cs | 247 +- Terminal.Gui/Views/Menu/MenuItemv2.cs | 11 +- Terminal.Gui/Views/Menu/Menuv2.cs | 36 +- Terminal.Gui/Views/Menu/PopoverMenu.cs | 59 +- Terminal.Gui/Views/RadioGroup.cs | 74 +- Terminal.Gui/Views/Shortcut.cs | 12 +- Terminal.sln.DotSettings | 1 + Tests/UnitTests/Dialogs/MessageBoxTests.cs | 2 +- Tests/UnitTests/Text/TextFormatterTests.cs | 2 +- .../View/Keyboard/KeyBindingsTests.cs | 22 + Tests/UnitTests/Views/MenuBarTests.cs | 4219 ++--------------- .../UnitTests/Views/Menuv1/MenuBarv1Tests.cs | 3886 +++++++++++++++ .../{MenuTests.cs => Menuv1/Menuv1Tests.cs} | 4 +- .../Views/FlagSelectorTests.cs | 124 + .../Views/MenuBarItemTests.cs | 22 + .../Views/MenuItemTests.cs | 14 + .../Views/MenuTests.cs | 17 + UICatalog/KeyBindingsDialog.cs | 211 - .../Scenarios/CharacterMap/CharacterMap.cs | 19 +- UICatalog/Scenarios/HexEditor.cs | 35 +- UICatalog/Scenarios/MenusV2.cs | 2 +- UICatalog/UICatalog.cs | 1317 +---- UICatalog/UICatalog.csproj | 2 +- UICatalog/UICatalogCommandLineOptions.cs | 18 + UICatalog/UICatalogTopLevel.cs | 721 +++ 31 files changed, 6388 insertions(+), 5137 deletions(-) create mode 100644 Terminal.Gui/Views/FlagSelector.cs create mode 100644 Terminal.Gui/Views/FlagSelectorStyles.cs create mode 100644 Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs rename Tests/UnitTests/Views/{MenuTests.cs => Menuv1/Menuv1Tests.cs} (97%) create mode 100644 Tests/UnitTestsParallelizable/Views/FlagSelectorTests.cs create mode 100644 Tests/UnitTestsParallelizable/Views/MenuBarItemTests.cs create mode 100644 Tests/UnitTestsParallelizable/Views/MenuItemTests.cs create mode 100644 Tests/UnitTestsParallelizable/Views/MenuTests.cs delete mode 100644 UICatalog/KeyBindingsDialog.cs create mode 100644 UICatalog/UICatalogCommandLineOptions.cs create mode 100644 UICatalog/UICatalogTopLevel.cs diff --git a/Terminal.Gui/Application/ApplicationPopover.cs b/Terminal.Gui/Application/ApplicationPopover.cs index bc4a0e0102..1124faefb2 100644 --- a/Terminal.Gui/Application/ApplicationPopover.cs +++ b/Terminal.Gui/Application/ApplicationPopover.cs @@ -126,8 +126,8 @@ public void Hide (IPopover? popover) // If there's an existing popover, hide it. if (_activePopover is View popoverView && popoverView == popover) { - popoverView.Visible = false; _activePopover = null; + popoverView.Visible = false; Application.Top?.SetNeedsDraw (); } } diff --git a/Terminal.Gui/View/View.Command.cs b/Terminal.Gui/View/View.Command.cs index 760315736a..8446c9f971 100644 --- a/Terminal.Gui/View/View.Command.cs +++ b/Terminal.Gui/View/View.Command.cs @@ -115,15 +115,18 @@ private void SetupCommands () /// protected bool? RaiseAccepting (ICommandContext? ctx) { + Logging.Trace($"{ctx?.Source?.Title}"); CommandEventArgs args = new () { Context = ctx }; // Best practice is to invoke the virtual method first. // This allows derived classes to handle the event and potentially cancel it. + Logging.Trace ($"Calling OnAccepting..."); args.Cancel = OnAccepting (args) || args.Cancel; if (!args.Cancel) { // If the event is not canceled by the virtual method, raise the event to notify any external subscribers. + Logging.Trace ($"Raising Accepting..."); Accepting?.Invoke (this, args); } @@ -148,11 +151,12 @@ private void SetupCommands () if (SuperView is { }) { - return SuperView?.InvokeCommand (Command.Accept, ctx) is true; + Logging.Trace ($"Invoking Accept on SuperView: {SuperView.Title}..."); + return SuperView?.InvokeCommand (Command.Accept, ctx); } } - return Accepting is null ? null : args.Cancel; + return args.Cancel; } /// diff --git a/Terminal.Gui/View/View.Keyboard.cs b/Terminal.Gui/View/View.Keyboard.cs index 095ff6946a..fcaf96914f 100644 --- a/Terminal.Gui/View/View.Keyboard.cs +++ b/Terminal.Gui/View/View.Keyboard.cs @@ -555,6 +555,11 @@ bool RaiseKeyUp (Key k) private static bool InvokeCommandsBoundToKeyOnAdornment (Adornment adornment, Key key, ref bool? handled) { + if (!adornment.Enabled) + { + return false; + } + bool? adornmentHandled = adornment.InvokeCommands (key); if (adornmentHandled is true) @@ -599,6 +604,11 @@ private static bool InvokeCommandsBoundToKeyOnAdornment (Adornment adornment, Ke /// internal bool? InvokeCommandsBoundToHotKey (Key hotKey) { + if (!Enabled) + { + return false; + } + bool? handled = null; // Process this View if (HotKeyBindings.TryGet (hotKey, out KeyBinding binding)) diff --git a/Terminal.Gui/Views/FlagSelector.cs b/Terminal.Gui/Views/FlagSelector.cs new file mode 100644 index 0000000000..97316f789b --- /dev/null +++ b/Terminal.Gui/Views/FlagSelector.cs @@ -0,0 +1,367 @@ +#nullable enable +namespace Terminal.Gui; + + +/// +/// Provides a user interface for displaying and selecting flags. +/// Flags can be set from a dictionary or directly from an enum type. +/// +public class FlagSelector : View, IDesignable, IOrientation +{ + /// + /// Initializes a new instance of the class. + /// + public FlagSelector () + { + CanFocus = true; + + Width = Dim.Auto (DimAutoStyle.Content); + Height = Dim.Auto (DimAutoStyle.Content); + + // ReSharper disable once UseObjectOrCollectionInitializer + _orientationHelper = new (this); + _orientationHelper.Orientation = Orientation.Vertical; + + // Accept (Enter key or DoubleClick) - Raise Accept event - DO NOT advance state + AddCommand (Command.Accept, HandleAcceptCommand); + + CreateSubViews (); + } + + private bool? HandleAcceptCommand (ICommandContext? ctx) { return RaiseAccepting (ctx); } + + private uint _value; + + /// + /// Gets or sets the value of the selected flags. + /// + public uint Value + { + get => _value; + set + { + if (_value == value) + { + return; + } + + _value = value; + + if (_value == 0) + { + UncheckAll (); + } + else + { + UncheckNone (); + UpdateChecked (); + } + + if (ValueEdit is { }) + { + ValueEdit.Text = value.ToString (); + } + + RaiseValueChanged (); + } + } + + private void RaiseValueChanged () + { + OnValueChanged (); + ValueChanged?.Invoke (this, new (Value)); + } + + /// + /// Called when has changed. + /// + protected virtual void OnValueChanged () { } + + /// + /// Raised when has changed. + /// + public event EventHandler>? ValueChanged; + + private FlagSelectorStyles _styles; + + /// + /// Gets or sets the styles for the flag selector. + /// + public FlagSelectorStyles Styles + { + get => _styles; + set + { + if (_styles == value) + { + return; + } + + _styles = value; + + CreateSubViews (); + } + } + + /// + /// Set the flags and flag names. + /// + /// + public void SetFlags (IReadOnlyDictionary flags) + { + Flags = flags; + CreateSubViews (); + } + + /// + /// Set the flags and flag names from an enum type. + /// + /// The enum type to extract flags from + /// + /// This is a convenience method that converts an enum to a dictionary of flag values and names. + /// The enum values are converted to uint values and the enum names become the display text. + /// + public void SetFlags () where TEnum : struct, Enum + { + // Convert enum names and values to a dictionary + Dictionary flagsDictionary = Enum.GetValues () + .ToDictionary ( + f => Convert.ToUInt32 (f), + f => f.ToString () + ); + + SetFlags (flagsDictionary); + } + + /// + /// Set the flags and flag names from an enum type with custom display names. + /// + /// The enum type to extract flags from + /// A function that converts enum values to display names + /// + /// This is a convenience method that converts an enum to a dictionary of flag values and custom names. + /// The enum values are converted to uint values and the display names are determined by the nameSelector function. + /// + /// + /// + /// // Use enum values with custom display names + /// var flagSelector = new FlagSelector (); + /// flagSelector.SetFlags<FlagSelectorStyles> + /// (f => f switch { + /// FlagSelectorStyles.ShowNone => "Show None Value", + /// FlagSelectorStyles.ShowValueEdit => "Show Value Editor", + /// FlagSelectorStyles.All => "Everything", + /// _ => f.ToString() + /// }); + /// + /// + public void SetFlags (Func nameSelector) where TEnum : struct, Enum + { + // Convert enum values and custom names to a dictionary + Dictionary flagsDictionary = Enum.GetValues () + .ToDictionary ( + f => Convert.ToUInt32 (f), + nameSelector + ); + + SetFlags (flagsDictionary); + } + + /// + /// Gets the flags. + /// + public IReadOnlyDictionary? Flags { get; internal set; } + + private TextField? ValueEdit { get; set; } + + private void CreateSubViews () + { + if (Flags is null) + { + return; + } + + View [] subviews = SubViews.ToArray (); + + RemoveAll (); + + foreach (View v in subviews) + { + v.Dispose (); + } + + if (Styles.HasFlag (FlagSelectorStyles.ShowNone) && !Flags.ContainsKey (0)) + { + Add (CreateCheckBox ("None", 0)); + } + + for (var index = 0; index < Flags.Count; index++) + { + if (!Styles.HasFlag (FlagSelectorStyles.ShowNone) && Flags.ElementAt (index).Key == 0) + { + continue; + } + + Add (CreateCheckBox (Flags.ElementAt (index).Value, Flags.ElementAt (index).Key)); + } + + if (Styles.HasFlag (FlagSelectorStyles.ShowValueEdit)) + { + ValueEdit = new () + { + Id = "valueEdit", + CanFocus = false, + Text = Value.ToString (), + Width = 5, + ReadOnly = true + }; + + Add (ValueEdit); + } + + SetLayout (); + + return; + + CheckBox CreateCheckBox (string name, uint flag) + { + var checkbox = new CheckBox + { + CanFocus = false, + Title = name, + Id = name, + Data = flag, + HighlightStyle = HighlightStyle + }; + + checkbox.Selecting += (sender, args) => { RaiseSelecting (args.Context); }; + + checkbox.CheckedStateChanged += (sender, args) => + { + uint newValue = Value; + + if (checkbox.CheckedState == CheckState.Checked) + { + if ((uint)checkbox.Data == 0) + { + newValue = 0; + } + else + { + newValue |= flag; + } + } + else + { + newValue &= ~flag; + } + + Value = newValue; + + //UpdateChecked(); + }; + + return checkbox; + } + } + + private void SetLayout () + { + foreach (View sv in SubViews) + { + if (Orientation == Orientation.Vertical) + { + sv.X = 0; + sv.Y = Pos.Align (Alignment.Start); + } + else + { + sv.X = Pos.Align (Alignment.Start); + sv.Y = 0; + sv.Margin!.Thickness = new (0, 0, 1, 0); + } + } + } + + private void UncheckAll () + { + foreach (CheckBox cb in SubViews.Where (sv => sv is CheckBox cb && cb.Title != "None").Cast ()) + { + cb.CheckedState = CheckState.UnChecked; + } + } + + private void UncheckNone () + { + foreach (CheckBox cb in SubViews.Where (sv => sv is CheckBox { Title: "None" }).Cast ()) + { + cb.CheckedState = CheckState.UnChecked; + } + } + + private void UpdateChecked () + { + foreach (CheckBox cb in SubViews.Where (sv => sv is CheckBox { }).Cast ()) + { + var flag = (uint)(cb.Data ?? throw new InvalidOperationException ("ComboBox.Data must be set")); + + // If this flag is set in Value, check the checkbox. Otherwise, uncheck it. + if (flag == 0 && Value != 0) + { + cb.CheckedState = CheckState.UnChecked; + } + else + { + cb.CheckedState = (Value & flag) == flag ? CheckState.Checked : CheckState.UnChecked; + } + } + } + + /// + protected override void OnSubViewAdded (View view) { } + + #region IOrientation + + /// + /// Gets or sets the for this . The default is + /// . + /// + public Orientation Orientation + { + get => _orientationHelper.Orientation; + set => _orientationHelper.Orientation = value; + } + + private readonly OrientationHelper _orientationHelper; + +#pragma warning disable CS0067 // The event is never used + /// + public event EventHandler>? OrientationChanging; + + /// + public event EventHandler>? OrientationChanged; +#pragma warning restore CS0067 // The event is never used + +#pragma warning restore CS0067 + + /// Called when has changed. + /// + public void OnOrientationChanged (Orientation newOrientation) { SetLayout (); } + + #endregion IOrientation + + /// + public bool EnableForDesign () + { + SetFlags ( + f => f switch + { + FlagSelectorStyles.ShowNone => "Show _None Value", + FlagSelectorStyles.ShowValueEdit => "Show _Value Editor", + FlagSelectorStyles.All => "Show _All Flags Selector", + _ => f.ToString () + }); + + return true; + } +} diff --git a/Terminal.Gui/Views/FlagSelectorStyles.cs b/Terminal.Gui/Views/FlagSelectorStyles.cs new file mode 100644 index 0000000000..95ea6f230d --- /dev/null +++ b/Terminal.Gui/Views/FlagSelectorStyles.cs @@ -0,0 +1,31 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Styles for . +/// +[Flags] +public enum FlagSelectorStyles +{ + /// + /// No styles. + /// + None = 0b_0000_0000, + + /// + /// Show the `None` checkbox. This will add a checkbox with the title "None" and a value of 0 + /// even if the flags do not contain a value of 0. + /// + ShowNone = 0b_0000_0001, + + /// + /// Show the value edit. This will add a read-only to the to allow + /// the user to see the value. + /// + ShowValueEdit = 0b_0000_0010, + + /// + /// All styles. + /// + All = ShowNone | ShowValueEdit +} diff --git a/Terminal.Gui/Views/Menu/MenuBarItemv2.cs b/Terminal.Gui/Views/Menu/MenuBarItemv2.cs index 0ed8cdfcd2..6df8e07593 100644 --- a/Terminal.Gui/Views/Menu/MenuBarItemv2.cs +++ b/Terminal.Gui/Views/Menu/MenuBarItemv2.cs @@ -77,13 +77,41 @@ public MenuBarItemv2 (string commandText, IEnumerable menuItems) new (menuItems)) { } - // TODO: Hide base.SubMenu? + /// + /// Do not use this property. MenuBarItem does not support SubMenu. Use instead. + /// + /// + public new Menuv2? SubMenu + { + get => null; + set => throw new InvalidOperationException ("MenuBarItem does not support SubMenu. Use PopoverMenu instead."); + } /// /// The Popover Menu that will be displayed when this item is selected. /// public PopoverMenu? PopoverMenu { get; set; } + /// + protected override bool OnKeyDownNotHandled (Key key) + { + Logging.Trace ($"{key}"); + + if (PopoverMenu is { Visible: true } && HotKeyBindings.TryGet (key, out _)) + { + // If the user presses the hotkey for a menu item that is already open, + // it should close the menu item (Test: MenuBarItem_HotKey_DeActivates) + if (SuperView is MenuBarv2 { } menuBar) + { + menuBar.HideActiveItem (); + } + + + return true; + } + return false; + } + /// protected override void Dispose (bool disposing) { diff --git a/Terminal.Gui/Views/Menu/MenuBarv2.cs b/Terminal.Gui/Views/Menu/MenuBarv2.cs index 6ad4560eb1..7850d673c5 100644 --- a/Terminal.Gui/Views/Menu/MenuBarv2.cs +++ b/Terminal.Gui/Views/Menu/MenuBarv2.cs @@ -1,4 +1,7 @@ #nullable enable +using System.ComponentModel; +using System.Diagnostics; + namespace Terminal.Gui; /// @@ -17,11 +20,56 @@ public MenuBarv2 () : this ([]) { } /// public MenuBarv2 (IEnumerable menuBarItems) : base (menuBarItems) { + CanFocus = false; TabStop = TabBehavior.TabGroup; Y = 0; Width = Dim.Fill (); Orientation = Orientation.Horizontal; + Key = DefaultKey; + AddCommand (Command.HotKey, + () => + { + if (HideActiveItem ()) + { + return true; + } + + if (SubViews.FirstOrDefault (sv => sv is MenuBarItemv2 { PopoverMenu: { } }) is MenuBarItemv2 { } first) + { + _active = true; + ShowPopover (first); + + return true; + } + + return false; + }); + HotKeyBindings.Add (Key, Command.HotKey); + + KeyBindings.Add (Key, Command.Quit); + KeyBindings.ReplaceCommands (Application.QuitKey, Command.Quit); + + AddCommand ( + Command.Quit, + ctx => + { + if (HideActiveItem ()) + { + return true; + } + + if (CanFocus) + { + CanFocus = false; + _active = false; + + return true; + } + + return false;//RaiseAccepted (ctx); + }); + AddCommand (Command.Right, MoveRight); KeyBindings.Add (Key.CursorRight, Command.Right); @@ -35,10 +83,108 @@ public MenuBarv2 (IEnumerable menuBarItems) : base (menuBarItems) bool? MoveRight (ICommandContext? ctx) { return AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); } } + private Key _key = DefaultKey; + + /// Specifies the key that will activate the context menu. + public Key Key + { + get => _key; + set + { + Key oldKey = _key; + _key = value; + KeyChanged?.Invoke (this, new (oldKey, _key)); + } + } + + /// + /// Sets the Menu Bar Items for this Menu Bar. This will replace any existing Menu Bar Items. + /// + /// + /// + /// This is a convenience property to help porting from the v1 MenuBar. + /// + /// + public MenuBarItemv2 []? Menus + { + set + { + RemoveAll (); + if (value is null) + { + return; + } + foreach (MenuBarItemv2 mbi in value) + { + Add (mbi); + } + } + } + + /// Raised when is changed. + public event EventHandler? KeyChanged; + + /// The default key for activating menu bars. + [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] + public static Key DefaultKey { get; set; } = Key.F9; + + /// + /// Gets whether any of the menu bar items have a visible . + /// + /// + public bool IsOpen () + { + return SubViews.Count (sv => sv is MenuBarItemv2 { PopoverMenu: { Visible: true } }) > 0; + } + + private bool _active; + + /// + /// Returns a value indicating whether the menu bar is active or not. When active, moving the mouse + /// over a menu bar item will activate it. + /// + /// + public bool IsActive () + { + return _active; + } + + /// + protected override bool OnMouseEnter (CancelEventArgs eventArgs) + { + // If the MenuBar does not have focus and the mouse enters: Enable CanFocus + // But do NOT show a Popover unless the user clicks or presses a hotkey + if (!HasFocus) + { + CanFocus = true; + } + return base.OnMouseEnter (eventArgs); + } + + /// + protected override void OnMouseLeave () + { + if (!IsOpen ()) + { + CanFocus = false; + } + base.OnMouseLeave (); + } + + /// + protected override void OnHasFocusChanged (bool newHasFocus, View? previousFocusedView, View? focusedView) + { + if (!newHasFocus) + { + _active = false; + CanFocus = false; + } + } + /// protected override void OnSelectedMenuItemChanged (MenuItemv2? selected) { - if (selected is MenuBarItemv2 { } selectedMenuBarItem) + if (selected is MenuBarItemv2 { PopoverMenu.Visible: false } selectedMenuBarItem) { ShowPopover (selectedMenuBarItem); } @@ -56,7 +202,7 @@ public override void EndInit () } // TODO: This needs to be done whenever a menuitem in any MenuBarItem changes - foreach (MenuBarItemv2? mbi in SubViews.Select(s => s as MenuBarItemv2)) + foreach (MenuBarItemv2? mbi in SubViews.Select (s => s as MenuBarItemv2)) { Application.Popover?.Register (mbi?.PopoverMenu); } @@ -65,36 +211,119 @@ public override void EndInit () /// protected override bool OnAccepting (CommandEventArgs args) { - if (args.Context?.Source is MenuBarItemv2 { PopoverMenu: { } } menuBarItem) + Logging.Trace ($"{args.Context?.Source?.Title}"); + + if (Visible && args.Context?.Source is MenuBarItemv2 { PopoverMenu.Visible: false } sourceMenuBarItem) { - ShowPopover (menuBarItem); + _active = true; + + if (!CanFocus) + { + // Enabling CanFocus will cause focus to change, which will cause OnSelectedMenuItem to change + // This will call ShowPopover + CanFocus = true; + sourceMenuBarItem.SetFocus (); + } + else + { + ShowPopover (sourceMenuBarItem); + } + + return true; } return base.OnAccepting (args); } + /// + /// Shows the specified popover, but only if the menu bar is active. + /// + /// private void ShowPopover (MenuBarItemv2? menuBarItem) { + Logging.Trace ($"{menuBarItem?.Id}"); + + if (!_active || !Visible) + { + return; + } + + //menuBarItem!.PopoverMenu.Id = menuBarItem.Id; + + // TODO: We should init the PopoverMenu in a smarter way if (menuBarItem?.PopoverMenu is { IsInitialized: false }) { menuBarItem.PopoverMenu.BeginInit (); menuBarItem.PopoverMenu.EndInit (); } - // If the active popover is a PopoverMenu and part of this MenuBar... - if (menuBarItem?.PopoverMenu is null - && Application.Popover?.GetActivePopover () is PopoverMenu popoverMenu + // If the active Application Popover is part of this MenuBar, hide it. + //HideActivePopover (); + if (Application.Popover?.GetActivePopover () is PopoverMenu popoverMenu && popoverMenu?.Root?.SuperMenuItem?.SuperView == this) { Application.Popover?.Hide (popoverMenu); } - menuBarItem?.PopoverMenu?.MakeVisible (new Point (menuBarItem.FrameToScreen ().X, menuBarItem.FrameToScreen ().Bottom)); + if (menuBarItem is null) + { + return; + } + + if (menuBarItem.PopoverMenu is { }) + { + menuBarItem.PopoverMenu.Accepted += (sender, args) => + { + if (HasFocus) + { + CanFocus = false; + } + }; + } - if (menuBarItem?.PopoverMenu?.Root is { }) + _active = true; + CanFocus = true; + menuBarItem.SetFocus (); + + if (menuBarItem.PopoverMenu?.Root is { }) { menuBarItem.PopoverMenu.Root.SuperMenuItem = menuBarItem; } + + menuBarItem.PopoverMenu?.MakeVisible (new Point (menuBarItem.FrameToScreen ().X, menuBarItem.FrameToScreen ().Bottom)); + } + + private MenuBarItemv2? GetActiveItem () + { + return SubViews.FirstOrDefault (sv => sv is MenuBarItemv2 { PopoverMenu: { Visible: true } }) as MenuBarItemv2; + } + + /// + /// Hides the popover menu associated with the active menu bar item and updates the focus state. + /// + /// if the popover was hidden + public bool HideActiveItem () + { + return HideItem (GetActiveItem ()); + } + + /// + /// Hides popover menu associated with the specified menu bar item and updates the focus state. + /// + /// + /// if the popover was hidden + public bool HideItem (MenuBarItemv2? activeItem) + { + if (activeItem is null || !activeItem.PopoverMenu!.Visible) + { + return false; + } + _active = false; + HasFocus = false; + activeItem.PopoverMenu!.Visible = false; + CanFocus = false; + + return true; } /// diff --git a/Terminal.Gui/Views/Menu/MenuItemv2.cs b/Terminal.Gui/Views/Menu/MenuItemv2.cs index f655d0b867..e57f962014 100644 --- a/Terminal.Gui/Views/Menu/MenuItemv2.cs +++ b/Terminal.Gui/Views/Menu/MenuItemv2.cs @@ -7,6 +7,7 @@ namespace Terminal.Gui; /// /// A -derived object to be used as a menu item in a . Has title, an +/// A -derived object to be used as a menu item in a . Has title, an /// associated help text, and an action to execute on activation. /// public class MenuItemv2 : Shortcut @@ -53,6 +54,11 @@ public MenuItemv2 (string? commandText = null, string? helpText = null, Action? : base (key ?? Key.Empty, commandText, action, helpText) { } + /// + public MenuItemv2 (string commandText, Key key, Action ? action = null) + : base (key ?? Key.Empty, commandText, action, null) + { } + /// public MenuItemv2 (string? commandText = null, string? helpText = null, Menuv2? subMenu = null) : base (Key.Empty, commandText, null, helpText) @@ -98,6 +104,7 @@ public Command Command internal override bool? DispatchCommand (ICommandContext? commandContext) { + Logging.Trace($"{commandContext?.Source?.Title}"); bool? ret = null; if (commandContext is { Command: not Command.HotKey }) @@ -116,11 +123,11 @@ public Command Command if (ret is not true) { + Logging.Trace($"Calling base.DispatchCommand"); ret = base.DispatchCommand (commandContext); } - //Logging.Trace ($"{commandContext?.Source?.Title}"); - + Logging.Trace($"Calling RaiseAccepted"); RaiseAccepted (commandContext); return ret; diff --git a/Terminal.Gui/Views/Menu/Menuv2.cs b/Terminal.Gui/Views/Menu/Menuv2.cs index 269acf4e44..a8a0b95742 100644 --- a/Terminal.Gui/Views/Menu/Menuv2.cs +++ b/Terminal.Gui/Views/Menu/Menuv2.cs @@ -10,7 +10,7 @@ public class Menuv2 : Bar public Menuv2 () : this ([]) { } /// - public Menuv2 (IEnumerable? shortcuts) : this (shortcuts?.Cast()) { } + public Menuv2 (IEnumerable? menuItems) : this (menuItems?.Cast ()) { } /// public Menuv2 (IEnumerable? shortcuts) : base (shortcuts) @@ -55,21 +55,21 @@ protected override void OnSubViewAdded (View view) switch (view) { case MenuItemv2 menuItem: - { - menuItem.CanFocus = true; + { + menuItem.CanFocus = true; - AddCommand (menuItem.Command, RaiseAccepted); + AddCommand (menuItem.Command, RaiseAccepted); - menuItem.Accepted += MenuItemOnAccepted; + menuItem.Accepted += MenuItemOnAccepted; - break; + break; - void MenuItemOnAccepted (object? sender, CommandEventArgs e) - { - //Logging.Trace ($"Accepted: {e.Context?.Source?.Title}"); - RaiseAccepted (e.Context); + void MenuItemOnAccepted (object? sender, CommandEventArgs e) + { + Logging.Trace ($"MenuItemOnAccepted: {e.Context?.Source?.Title}"); + RaiseAccepted (e.Context); + } } - } case Line line: // Grow line so we get auto-join line line.X = Pos.Func (() => -Border!.Thickness.Left); @@ -79,6 +79,19 @@ void MenuItemOnAccepted (object? sender, CommandEventArgs e) } } + /// + protected override bool OnAccepting (CommandEventArgs args) + { + Logging.Trace ($"{args.Context}"); + + if (SuperMenuItem is { }) + { + Logging.Trace ($"Invoking Accept on SuperMenuItem: {SuperMenuItem.Title}..."); + return SuperMenuItem?.SuperView?.InvokeCommand (Command.Accept, args.Context) is true; + } + return false; + } + // TODO: Consider moving Accepted to Bar? /// @@ -120,6 +133,7 @@ protected virtual void OnAccepted (CommandEventArgs args) { } protected override void OnFocusedChanged (View? previousFocused, View? focused) { base.OnFocusedChanged (previousFocused, focused); + SelectedMenuItem = focused as MenuItemv2; RaiseSelectedMenuItemChanged (SelectedMenuItem); } diff --git a/Terminal.Gui/Views/Menu/PopoverMenu.cs b/Terminal.Gui/Views/Menu/PopoverMenu.cs index 555dd9175b..a725750f74 100644 --- a/Terminal.Gui/Views/Menu/PopoverMenu.cs +++ b/Terminal.Gui/Views/Menu/PopoverMenu.cs @@ -3,7 +3,8 @@ namespace Terminal.Gui; /// /// Provides a cascading menu that pops over all other content. Can be used as a context menu or a drop-down -/// menu as part of . +/// all other content. Can be used as a context menu or a drop-down +/// menu as part of as part of . /// /// /// @@ -33,8 +34,6 @@ public PopoverMenu (Menuv2? root) base.Visible = false; - //base.ColorScheme = Colors.ColorSchemes ["Menu"]; - Root = root; AddCommand (Command.Right, MoveRight); @@ -53,7 +52,7 @@ public PopoverMenu (Menuv2? root) return false; }); - KeyBindings.Add (DefaultKey, Command.Quit); + KeyBindings.Add (Key, Command.Quit); KeyBindings.ReplaceCommands (Application.QuitKey, Command.Quit); AddCommand ( @@ -67,7 +66,7 @@ public PopoverMenu (Menuv2? root) Visible = false; - return RaiseAccepted (ctx); + return false; }); return; @@ -98,7 +97,7 @@ public PopoverMenu (Menuv2? root) return true; } - return AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); + return false; //AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop); } } @@ -220,8 +219,11 @@ public Menuv2? Root _root.Accepting += MenuOnAccepting; } + // TODO: This needs to be done whenever any MenuItem in the menu tree changes to support dynamic menus + // TODO: And it needs to clear the old bindings first UpdateKeyBindings (); + // TODO: This needs to be done whenever any MenuItem in the menu tree changes to support dynamic menus IEnumerable allMenus = GetAllSubMenus (); foreach (Menuv2 menu in allMenus) @@ -235,13 +237,12 @@ public Menuv2? Root private void UpdateKeyBindings () { - // TODO: This needs to be done whenever any MenuItem in the menu tree changes to support dynamic menus - // TODO: And it needs to clear them first IEnumerable all = GetMenuItemsOfAllSubMenus (); foreach (MenuItemv2 menuItem in all.Where (mi => mi.Command != Command.NotBound)) { Key? key; + if (menuItem.TargetView is { }) { // A TargetView implies HotKey @@ -447,17 +448,16 @@ private void HideAndRemoveSubMenu (Menuv2? menu) private void MenuOnAccepting (object? sender, CommandEventArgs e) { + var senderView = sender as View; + Logging.Trace ($"Sender: {senderView?.GetType ().Name}, {e.Context?.Source?.Title}"); + if (e.Context?.Command != Command.HotKey) { Visible = false; } - else - { - // This supports the case when a hotkey of a menuitem with a submenu is pressed - e.Cancel = true; - } - //Logging.Trace ($"{e.Context?.Source?.Title}"); + // This supports the case when a hotkey of a menuitem with a submenu is pressed + //e.Cancel = true; } private void MenuAccepted (object? sender, CommandEventArgs e) @@ -514,10 +514,21 @@ protected virtual void OnAccepted (CommandEventArgs args) { } private void MenuOnSelectedMenuItemChanged (object? sender, MenuItemv2? e) { - //Logging.Trace ($"{e}"); + Logging.Trace ($"e: {e?.Title}"); ShowSubMenu (e); } + /// + protected override void OnSubViewAdded (View view) + { + if (Root is null && (view is Menuv2 || view is MenuItemv2)) + { + throw new InvalidOperationException ("Do not add MenuItems or Menus directly to a PopoverMenu. Use the Root property."); + } + + base.OnSubViewAdded (view); + } + /// protected override void Dispose (bool disposing) { @@ -539,20 +550,20 @@ protected override void Dispose (bool disposing) base.Dispose (disposing); } - /// public bool EnableForDesign (ref readonly TContext context) where TContext : notnull { - Root = new Menuv2 ( - [ - new MenuItemv2 (this, Command.Cut), - new MenuItemv2 (this, Command.Copy), - new MenuItemv2 (this, Command.Paste), - new Line (), - new MenuItemv2 (this, Command.SelectAll) - ]); + Root = new ( + [ + new MenuItemv2 (this, Command.Cut), + new MenuItemv2 (this, Command.Copy), + new MenuItemv2 (this, Command.Paste), + new Line (), + new MenuItemv2 (this, Command.SelectAll) + ]); Visible = true; + return true; } } diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs index 5fc2b2c1b2..daefd8b442 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -1,9 +1,7 @@ #nullable enable -using System.Diagnostics; - namespace Terminal.Gui; -/// Displays a group of labels with an idicator of which one is selected. +/// Displays a list of mutually-exclusive items. Each items can have its own hotkey. public class RadioGroup : View, IDesignable, IOrientation { /// @@ -19,7 +17,7 @@ public RadioGroup () // Select (Space key or mouse click) - The default implementation sets focus. RadioGroup does not. AddCommand (Command.Select, HandleSelectCommand); - // Accept (Enter key or Doubleclick) - Raise Accept event - DO NOT advance state + // Accept (Enter key or DoubleClick) - Raise Accept event - DO NOT advance state AddCommand (Command.Accept, HandleAcceptCommand); // Hotkey - ctx may indicate a radio item hotkey was pressed. Behavior depends on HasFocus @@ -59,7 +57,7 @@ public RadioGroup () if (HasFocus) { - if ((item is null || HotKey == keyCommandContext.Binding.Key?.NoAlt.NoCtrl.NoShift!)) + if (item is null || HotKey == keyCommandContext.Binding.Key?.NoAlt.NoCtrl.NoShift!) { // It's this.HotKey OR Another View (Label?) forwarded the hotkey command to us - Act just like `Space` (Select) return InvokeCommand (Command.Select); @@ -145,14 +143,14 @@ public RadioGroup () if (c > -1) { // Just like the user pressing the items' hotkey - return InvokeCommand (Command.HotKey, new KeyBinding ([Command.HotKey], target: this, data: c)) == true; + return InvokeCommand (Command.HotKey, new KeyBinding ([Command.HotKey], this, c)) == true; } } return false; } - bool cursorChanged = false; + var cursorChanged = false; if (SelectedItem == Cursor) { @@ -164,7 +162,7 @@ public RadioGroup () } } - bool selectedItemChanged = false; + var selectedItemChanged = false; if (SelectedItem != Cursor) { @@ -209,7 +207,8 @@ private void SetupKeyBindings () } /// - /// Gets or sets whether double clicking on a Radio Item will cause the event to be raised. + /// Gets or sets whether double-clicking on a Radio Item will cause the event to be + /// raised. /// /// /// @@ -241,7 +240,21 @@ public int HorizontalSpace } } - private List _radioLabels = []; + /// + /// If the will each be automatically assigned a hotkey. + /// will be used to ensure unique keys are assigned. Set + /// before setting with any hotkeys that may conflict with other Views. + /// + public bool AssignHotKeysToRadioLabels { get; set; } + + /// + /// Gets the list of hotkeys already used by or that should not be used if + /// + /// is enabled. + /// + public List UsedHotKeys { get; } = []; + + private readonly List _radioLabels = []; /// /// The radio labels to display. A key binding will be added for each label enabling the @@ -263,16 +276,40 @@ public string [] RadioLabels } } - int prevCount = _radioLabels.Count; - _radioLabels = value.ToList (); + _radioLabels.Clear (); - for (var index = 0; index < _radioLabels.Count; index++) + // Pick a unique hotkey for each radio label + for (var labelIndex = 0; labelIndex < value.Length; labelIndex++) { - string label = _radioLabels [index]; + string label = value [labelIndex]; + string? newLabel = label; - if (TextFormatter.FindHotKey (label, HotKeySpecifier, out _, out Key hotKey)) + if (AssignHotKeysToRadioLabels) { - AddKeyBindingsForHotKey (Key.Empty, hotKey, index); + // Find the first char in label that is [a-z], [A-Z], or [0-9] + for (var i = 0; i < label.Length; i++) + { + if (UsedHotKeys.Contains (new (label [i])) || !char.IsAsciiLetterOrDigit (label [i])) + { + continue; + } + + if (char.IsAsciiLetterOrDigit (label [i])) + { + char? hotChar = label [i]; + newLabel = label.Insert (i, HotKeySpecifier.ToString ()); + UsedHotKeys.Add (new (hotChar)); + + break; + } + } + } + + _radioLabels.Add (newLabel); + + if (TextFormatter.FindHotKey (newLabel, HotKeySpecifier, out _, out Key hotKey)) + { + AddKeyBindingsForHotKey (Key.Empty, hotKey, labelIndex); } } @@ -351,7 +388,7 @@ protected override bool OnDrawingContent () if (j == hotPos && i == Cursor) { - SetAttribute (HasFocus ? GetHotFocusColor() : GetHotNormalColor ()); + SetAttribute (HasFocus ? GetHotFocusColor () : GetHotNormalColor ()); } else if (j == hotPos && i != Cursor) { @@ -369,7 +406,7 @@ protected override bool OnDrawingContent () if (i == Cursor) { - SetAttribute (HasFocus ? GetHotFocusColor() : GetHotNormalColor ()); + SetAttribute (HasFocus ? GetHotFocusColor () : GetHotNormalColor ()); } else if (i != Cursor) { @@ -386,6 +423,7 @@ protected override bool OnDrawingContent () DrawHotString (rl, HasFocus && i == Cursor); } } + return true; } diff --git a/Terminal.Gui/Views/Shortcut.cs b/Terminal.Gui/Views/Shortcut.cs index 11f4e51754..7f98a94004 100644 --- a/Terminal.Gui/Views/Shortcut.cs +++ b/Terminal.Gui/Views/Shortcut.cs @@ -103,9 +103,6 @@ public Shortcut (Key key, string? commandText, Action? action, string? helpText ShowHide (); } - /// - protected override bool OnClearingViewport () { return base.OnClearingViewport (); } - // Helper to set Width consistently internal Dim GetWidthDimAuto () { @@ -269,6 +266,7 @@ private void AddCommands () /// internal virtual bool? DispatchCommand (ICommandContext? commandContext) { + Logging.Trace($"{commandContext?.Source?.Title}"); CommandContext? keyCommandContext = commandContext as CommandContext? ?? default (CommandContext); if (keyCommandContext?.Binding.Data != this) @@ -276,16 +274,21 @@ private void AddCommands () // Invoke Select on the CommandView to cause it to change state if it wants to // If this causes CommandView to raise Accept, we eat it keyCommandContext = keyCommandContext!.Value with { Binding = keyCommandContext.Value.Binding with { Data = this } }; + + Logging.Trace ($"Invoking Select on CommandView."); + CommandView.InvokeCommand (Command.Select, keyCommandContext); } // BUGBUG: Why does this use keyCommandContext and not commandContext? + Logging.Trace ($"RaiseSelecting ..."); if (RaiseSelecting (keyCommandContext) is true) { return true; } // The default HotKey handler sets Focus + Logging.Trace ($"SetFocus..."); SetFocus (); var cancel = false; @@ -294,6 +297,7 @@ private void AddCommands () { commandContext.Source = this; } + Logging.Trace ($"RaiseAccepting..."); cancel = RaiseAccepting (commandContext) is true; if (cancel) @@ -308,6 +312,7 @@ private void AddCommands () if (Action is { }) { + Logging.Trace ($"Invoke Action..."); Action.Invoke (); // Assume if there's a subscriber to Action, it's handled. @@ -496,6 +501,7 @@ private void Shortcut_TitleChanged (object? sender, EventArgs e) // This is a helper to make it easier to set the CommandView text. // CommandView is public and replaceable, but this is a convenience. _commandView.Text = Title; + //_commandView.Title = Title; } #endregion Command diff --git a/Terminal.sln.DotSettings b/Terminal.sln.DotSettings index 5102526225..eb3995de14 100644 --- a/Terminal.sln.DotSettings +++ b/Terminal.sln.DotSettings @@ -394,6 +394,7 @@ <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> <Policy><Descriptor Staticness="Any" AccessRightKinds="Private" Description="Constant fields (private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /></Policy> <Policy><Descriptor Staticness="Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Instance fields (not private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Constant fields (not private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /></Policy> <Policy><Descriptor Staticness="Static" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Static fields (not private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy></Policy> <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local constants"><ElementKinds><Kind Name="LOCAL_CONSTANT" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /></Policy> diff --git a/Tests/UnitTests/Dialogs/MessageBoxTests.cs b/Tests/UnitTests/Dialogs/MessageBoxTests.cs index 3b93d98191..9216b863ee 100644 --- a/Tests/UnitTests/Dialogs/MessageBoxTests.cs +++ b/Tests/UnitTests/Dialogs/MessageBoxTests.cs @@ -462,7 +462,7 @@ public void UICatalog_AboutBox () { MessageBox.Query ( "", - UICatalogApp.GetAboutBoxMessage (), + UICatalog.UICatalogTopLevel.GetAboutBoxMessage (), wrapMessage: false, buttons: "_Ok" ); diff --git a/Tests/UnitTests/Text/TextFormatterTests.cs b/Tests/UnitTests/Text/TextFormatterTests.cs index d214dffb8d..fe6dc292ae 100644 --- a/Tests/UnitTests/Text/TextFormatterTests.cs +++ b/Tests/UnitTests/Text/TextFormatterTests.cs @@ -4146,7 +4146,7 @@ public void UICatalog_AboutBox_Text () { TextFormatter tf = new () { - Text = UICatalogApp.GetAboutBoxMessage (), + Text = UICatalog.UICatalogTopLevel.GetAboutBoxMessage (), Alignment = Alignment.Center, VerticalAlignment = Alignment.Start, WordWrap = false, diff --git a/Tests/UnitTests/View/Keyboard/KeyBindingsTests.cs b/Tests/UnitTests/View/Keyboard/KeyBindingsTests.cs index 64685a9d64..d90ac3240c 100644 --- a/Tests/UnitTests/View/Keyboard/KeyBindingsTests.cs +++ b/Tests/UnitTests/View/Keyboard/KeyBindingsTests.cs @@ -129,6 +129,28 @@ public void HotKey_KeyBinding_Negative () top.Dispose (); } + [Fact] + [AutoInitShutdown] + public void HotKey_Enabled_False_Does_Not_Invoke () + { + var view = new ScopedKeyBindingView (); + var keyWasHandled = false; + view.KeyDownNotHandled += (s, e) => keyWasHandled = true; + + var top = new Toplevel (); + top.Add (view); + Application.Begin (top); + + Application.RaiseKeyDownEvent (Key.Z); + Assert.False (keyWasHandled); + Assert.False (view.HotKeyCommand); + + keyWasHandled = false; + view.Enabled = false; + Application.RaiseKeyDownEvent (Key.F); + Assert.False (view.HotKeyCommand); + top.Dispose (); + } // tests that test KeyBindingScope.Focus and KeyBindingScope.HotKey (tests for KeyBindingScope.Application are in Application/KeyboardTests.cs) public class ScopedKeyBindingView : View diff --git a/Tests/UnitTests/Views/MenuBarTests.cs b/Tests/UnitTests/Views/MenuBarTests.cs index b65fdf9968..8fb2b55e6c 100644 --- a/Tests/UnitTests/Views/MenuBarTests.cs +++ b/Tests/UnitTests/Views/MenuBarTests.cs @@ -3,3884 +3,691 @@ namespace Terminal.Gui.ViewsTests; -public class MenuBarTests (ITestOutputHelper output) +public class MenuBarTests () { [Fact] [AutoInitShutdown] - public void AddMenuBarItem_RemoveMenuItem_Dynamically () + public void DefaultKey_Activates () { - var menuBar = new MenuBar (); - var menuBarItem = new MenuBarItem { Title = "_New" }; - var action = ""; - var menuItem = new MenuItem { Title = "_Item", Action = () => action = "I", Parent = menuBarItem }; - Assert.Equal ("n", menuBarItem.HotKey); - Assert.Equal ("i", menuItem.HotKey); - Assert.Empty (menuBar.Menus); - menuBarItem.AddMenuBarItem (menuBar, menuItem); - menuBar.Menus = [menuBarItem]; - Assert.Single (menuBar.Menus); - Assert.Single (menuBar.Menus [0].Children!); - - Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); - Assert.False (menuBar.HotKeyBindings.TryGet (Key.I, out _)); - + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); var top = new Toplevel (); top.Add (menuBar); - Application.Begin (top); - - top.NewKeyDownEvent (Key.N.WithAlt); - Application.MainLoop.RunIteration (); - Assert.True (menuBar.IsMenuOpen); - Assert.Equal ("", action); - - top.NewKeyDownEvent (Key.I); - Application.MainLoop.RunIteration (); - Assert.False (menuBar.IsMenuOpen); - Assert.Equal ("I", action); - - menuItem.RemoveMenuItem (); - Assert.Single (menuBar.Menus); - Assert.Null (menuBar.Menus [0].Children); - Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); - Assert.False (menuBar.HotKeyBindings.TryGet (Key.I, out _)); - - menuBarItem.RemoveMenuItem (); - Assert.Empty (menuBar.Menus); - Assert.False (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); - - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void AllowNullChecked_Get_Set () - { - var mi = new MenuItem ("Check this out 你", "", null) { CheckType = MenuItemCheckStyle.Checked }; - mi.Action = mi.ToggleChecked; + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); - var menu = new MenuBar - { - Menus = - [ - new ("Nullable Checked", new [] { mi }) - ] - }; + // Act + Application.RaiseKeyDownEvent (MenuBarv2.DefaultKey); + Assert.True (menuBar.IsActive ()); + Assert.True (menuBar.IsOpen ()); + Assert.True (menuBar.HasFocus); + Assert.True (menuBar.CanFocus); + Assert.True (menuBarItem.PopoverMenu.Visible); + Assert.True (menuBarItem.PopoverMenu.HasFocus); - //new CheckBox (); - Toplevel top = new (); - top.Add (menu); - Application.Begin (top); - - Assert.False (mi.Checked); - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu._openMenu.NewKeyDownEvent (Key.Enter)); - Application.MainLoop.RunIteration (); - Assert.True (mi.Checked); - - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } - ) - ); - - Assert.True ( - menu._openMenu.NewMouseEvent ( - new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked, View = menu._openMenu } - ) - ); - Application.MainLoop.RunIteration (); - Assert.False (mi.Checked); - - mi.AllowNullChecked = true; - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu._openMenu.NewKeyDownEvent (Key.Enter)); - Application.MainLoop.RunIteration (); - Assert.Null (mi.Checked); - - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } - ) - ); - Application.LayoutAndDraw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @$" - Nullable Checked -┌──────────────────────┐ -│ {Glyphs.CheckStateNone} Check this out 你 │ -└──────────────────────┘", - output - ); - - Assert.True ( - menu._openMenu.NewMouseEvent ( - new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked, View = menu._openMenu } - ) - ); - Application.MainLoop.RunIteration (); - Assert.True (mi.Checked); - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu._openMenu.NewKeyDownEvent (Key.Enter)); - Application.MainLoop.RunIteration (); - Assert.False (mi.Checked); - - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } - ) - ); - - Assert.True ( - menu._openMenu.NewMouseEvent ( - new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked, View = menu._openMenu } - ) - ); - Application.MainLoop.RunIteration (); - Assert.Null (mi.Checked); - - mi.AllowNullChecked = false; - Assert.False (mi.Checked); - - mi.CheckType = MenuItemCheckStyle.NoCheck; - Assert.Throws (mi.ToggleChecked); - - mi.CheckType = MenuItemCheckStyle.Radio; - Assert.Throws (mi.ToggleChecked); + Application.End (rs); top.Dispose (); } [Fact] [AutoInitShutdown] - public void CanExecute_False_Does_Not_Throws () + public void DefaultKey_Deactivates () { - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] - { - new ("New", "", null, () => false), - null, - new ("Quit", "", null) - }) - ] - }; + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void CanExecute_HotKey () - { - Window win = null; - - var menu = new MenuBar - { - Menus = - [ - new ( - "_File", - new MenuItem [] - { - new ("_New", "", New, CanExecuteNew), - new ( - "_Close", - "", - Close, - CanExecuteClose - ) - } - ) - ] - }; - Toplevel top = new (); - top.Add (menu); - - bool CanExecuteNew () { return win == null; } - - void New () { win = new (); } - - bool CanExecuteClose () { return win != null; } - - void Close () { win = null; } + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); - Application.Begin (top); + // Act + Application.RaiseKeyDownEvent (MenuBarv2.DefaultKey); + Assert.True (menuBar.IsOpen ()); + Assert.True (menuBarItem.PopoverMenu.Visible); - Assert.Null (win); - Assert.True (CanExecuteNew ()); - Assert.False (CanExecuteClose ()); + Application.RaiseKeyDownEvent (MenuBarv2.DefaultKey); + Assert.False (menuBar.IsActive ()); + Assert.False (menuBar.IsOpen ()); + Assert.False (menuBar.HasFocus); + Assert.False (menuBar.CanFocus); + Assert.False (menuBarItem.PopoverMenu.Visible); + Assert.False (menuBarItem.PopoverMenu.HasFocus); - Assert.True (top.NewKeyDownEvent (Key.F.WithAlt)); - Assert.True (top.NewKeyDownEvent (Key.N.WithAlt)); - Application.MainLoop.RunIteration (); - Assert.NotNull (win); - Assert.False (CanExecuteNew ()); - Assert.True (CanExecuteClose ()); + Application.End (rs); top.Dispose (); } [Fact] [AutoInitShutdown] - public void Click_Another_View_Close_An_Open_Menu () + public void QuitKey_DeActivates () { - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }) - ] - }; - - var btnClicked = false; - var btn = new Button { Y = 4, Text = "Test" }; - btn.Accepting += (s, e) => btnClicked = true; + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); var top = new Toplevel (); - top.Add (menu, btn); - Application.Begin (top); - - Application.RaiseMouseEvent (new () { ScreenPosition = new (0, 4), Flags = MouseFlags.Button1Clicked }); - Assert.True (btnClicked); - top.Dispose (); - } - - // TODO: Lots of tests in here really test Menu and MenuItem - Move them to MenuTests.cs - - [Fact] - public void Constructors_Defaults () - { - var menuBar = new MenuBar (); - Assert.Equal (KeyCode.F9, menuBar.Key); - var menu = new Menu { Host = menuBar, X = 0, Y = 0, BarItems = new () }; - Assert.Null (menu.ColorScheme); - Assert.False (menu.IsInitialized); - menu.BeginInit (); - menu.EndInit (); - Assert.Equal (Colors.ColorSchemes ["Menu"], menu.ColorScheme); - Assert.True (menu.CanFocus); - Assert.False (menu.WantContinuousButtonPressed); - Assert.Equal (LineStyle.Single, menuBar.MenusBorderStyle); - - menuBar = new (); - Assert.Equal (0, menuBar.X); - Assert.Equal (0, menuBar.Y); - Assert.IsType (menuBar.Width); - Assert.Equal (1, menuBar.Height); - Assert.Empty (menuBar.Menus); - Assert.Equal (Colors.ColorSchemes ["Menu"], menuBar.ColorScheme); - Assert.True (menuBar.WantMousePositionReports); - Assert.False (menuBar.IsMenuOpen); - - menuBar = new () { Menus = [] }; - Assert.Equal (0, menuBar.X); - Assert.Equal (0, menuBar.Y); - Assert.IsType (menuBar.Width); - Assert.Equal (1, menuBar.Height); - Assert.Empty (menuBar.Menus); - Assert.Equal (Colors.ColorSchemes ["Menu"], menuBar.ColorScheme); - Assert.True (menuBar.WantMousePositionReports); - Assert.False (menuBar.IsMenuOpen); - - var menuBarItem = new MenuBarItem (); - Assert.Equal ("", menuBarItem.Title); - Assert.Null (menuBarItem.Parent); - Assert.Empty (menuBarItem.Children); - - menuBarItem = new (new MenuBarItem [] { }); - Assert.Equal ("", menuBarItem.Title); - Assert.Null (menuBarItem.Parent); - Assert.Empty (menuBarItem.Children); - - menuBarItem = new ("Test", new MenuBarItem [] { }); - Assert.Equal ("Test", menuBarItem.Title); - Assert.Null (menuBarItem.Parent); - Assert.Empty (menuBarItem.Children); - - menuBarItem = new ("Test", new List ()); - Assert.Equal ("Test", menuBarItem.Title); - Assert.Null (menuBarItem.Parent); - Assert.Empty (menuBarItem.Children); - - menuBarItem = new ("Test", "Help", null); - Assert.Equal ("Test", menuBarItem.Title); - Assert.Equal ("Help", menuBarItem.Help); - Assert.Null (menuBarItem.Action); - Assert.Null (menuBarItem.CanExecute); - Assert.Null (menuBarItem.Parent); - Assert.Equal (Key.Empty, menuBarItem.ShortcutKey); - } - - [Fact] - [AutoInitShutdown (configLocation: ConfigLocations.Default)] - public void Disabled_MenuBar_Is_Never_Opened () - { - Toplevel top = new (); - - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }) - ] - }; - top.Add (menu); - Application.Begin (top); - Assert.True (menu.Enabled); - menu.OpenMenu (); - Assert.True (menu.IsMenuOpen); - - menu.Enabled = false; - menu.CloseAllMenus (); - menu.OpenMenu (); - Assert.False (menu.IsMenuOpen); - top.Dispose (); - } - - [Fact (Skip = "#3798 Broke. Will fix in #2975")] - [AutoInitShutdown (configLocation: ConfigLocations.Default)] - public void Disabled_MenuItem_Is_Never_Selected () - { - var menu = new MenuBar - { - Menus = - [ - new ( - "Menu", - new MenuItem [] - { - new ("Enabled 1", "", null), - new ("Disabled", "", null, () => false), - null, - new ("Enabled 2", "", null) - } - ) - ] - }; - - Toplevel top = new (); - top.Add (menu); - Application.Begin (top); - - Attribute [] attributes = - { - // 0 - menu.ColorScheme.Normal, - - // 1 - menu.ColorScheme.Focus, - - // 2 - menu.ColorScheme.Disabled - }; - - DriverAssert.AssertDriverAttributesAre ( - @" -00000000000000", - output, - Application.Driver, - attributes - ); - - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } - ) - ); - top.Draw (); - - DriverAssert.AssertDriverAttributesAre ( - @" -11111100000000 -00000000000000 -01111111111110 -02222222222220 -00000000000000 -00000000000000 -00000000000000", - output, - Application.Driver, - attributes - ); - - Assert.True ( - top.SubViews.ElementAt (1) - .NewMouseEvent ( - new () { Position = new (0, 2), Flags = MouseFlags.Button1Clicked, View = top.SubViews.ElementAt (1) } - ) - ); - top.SubViews.ElementAt (1).Layout(); - top.SubViews.ElementAt (1).Draw (); - - DriverAssert.AssertDriverAttributesAre ( - @" -11111100000000 -00000000000000 -01111111111110 -02222222222220 -00000000000000 -00000000000000 -00000000000000", - output, - Application.Driver, - attributes - ); - - Assert.True ( - top.SubViews.ElementAt (1) - .NewMouseEvent ( - new () { Position = new (0, 2), Flags = MouseFlags.ReportMousePosition, View = top.SubViews.ElementAt (1) } - ) - ); - top.SubViews.ElementAt (1).Draw (); - - DriverAssert.AssertDriverAttributesAre ( - @" -11111100000000 -00000000000000 -01111111111110 -02222222222220 -00000000000000 -00000000000000 -00000000000000", - output, - Application.Driver, - attributes - ); - top.Dispose (); - } - - [Fact (Skip = "#3798 Broke. Will fix in #2975")] - [AutoInitShutdown] - public void Draw_A_Menu_Over_A_Dialog () - { - // Override CM - Window.DefaultBorderStyle = LineStyle.Single; - Dialog.DefaultButtonAlignment = Alignment.Center; - Dialog.DefaultBorderStyle = LineStyle.Single; - Dialog.DefaultShadow = ShadowStyle.None; - Button.DefaultShadow = ShadowStyle.None; - - Toplevel top = new (); - var win = new Window (); - top.Add (win); - RunState rsTop = Application.Begin (top); - ((FakeDriver)Application.Driver!).SetBufferSize (40, 15); - - Assert.Equal (new (0, 0, 40, 15), win.Frame); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - List items = new () - { - "New", - "Open", - "Close", - "Save", - "Save As", - "Delete" - }; - var dialog = new Dialog { X = 2, Y = 2, Width = 15, Height = 4 }; - var menu = new MenuBar { X = Pos.Center (), Width = 10 }; - - menu.Menus = new MenuBarItem [] - { - new ( - "File", - new MenuItem [] - { - new ( - items [0], - "Create a new file", - () => ChangeMenuTitle ("New"), - null, - null, - KeyCode.CtrlMask | KeyCode.N - ), - new ( - items [1], - "Open a file", - () => ChangeMenuTitle ("Open"), - null, - null, - KeyCode.CtrlMask | KeyCode.O - ), - new ( - items [2], - "Close a file", - () => ChangeMenuTitle ("Close"), - null, - null, - KeyCode.CtrlMask | KeyCode.C - ), - new ( - items [3], - "Save a file", - () => ChangeMenuTitle ("Save"), - null, - null, - KeyCode.CtrlMask | KeyCode.S - ), - new ( - items [4], - "Save a file as", - () => ChangeMenuTitle ("Save As"), - null, - null, - KeyCode.CtrlMask | KeyCode.A - ), - new ( - items [5], - "Delete a file", - () => ChangeMenuTitle ("Delete"), - null, - null, - KeyCode.CtrlMask | KeyCode.A - ) - } - ) - }; - dialog.Add (menu); - - void ChangeMenuTitle (string title) - { - menu.Menus [0].Title = title; - menu.SetNeedsDraw (); - } - - RunState rsDialog = Application.Begin (dialog); - Application.RunIteration (ref rsDialog); - - Assert.Equal (new (2, 2, 15, 4), dialog.Frame); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ │ -│ ┌─────────────┐ │ -│ │ File │ │ -│ │ │ │ -│ └─────────────┘ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.Equal ("File", menu.Menus [0].Title); - menu.OpenMenu (); - Application.RunIteration (ref rsDialog); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ │ -│ ┌─────────────┐ │ -│ │ File │ │ -│ │ ┌──────────────────────────────────┐ -│ └─│ New Create a new file Ctrl+N │ -│ │ Open Open a file Ctrl+O │ -│ │ Close Close a file Ctrl+C │ -│ │ Save Save a file Ctrl+S │ -│ │ Save As Save a file as Ctrl+A │ -│ │ Delete Delete a file Ctrl+A │ -│ └──────────────────────────────────┘ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Application.RaiseMouseEvent (new () { ScreenPosition = new (20, 5), Flags = MouseFlags.Button1Clicked }); - - // Need to fool MainLoop into thinking it's running - Application.MainLoop.Running = true; - bool firstIteration = true; - Application.RunIteration (ref rsDialog, firstIteration); - Assert.Equal (items [0], menu.Menus [0].Title); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ │ -│ ┌─────────────┐ │ -│ │ New │ │ -│ │ │ │ -│ └─────────────┘ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - for (var i = 0; i < items.Count; i++) - { - menu.OpenMenu (); - - Application.RaiseMouseEvent (new () { ScreenPosition = new (20, 5 + i), Flags = MouseFlags.Button1Clicked }); - - Application.RunIteration (ref rsDialog); - Assert.Equal (items [i], menu.Menus [0].Title); - } - - ((FakeDriver)Application.Driver!).SetBufferSize (20, 15); - menu.OpenMenu (); - Application.RunIteration (ref rsDialog); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────┐ -│ │ -│ ┌─────────────┐ │ -│ │ Delete │ │ -│ │ ┌─────────────── -│ └─│ New Create -│ │ Open O -│ │ Close Cl -│ │ Save S -│ │ Save As Save -│ │ Delete Del -│ └─────────────── -│ │ -│ │ -└──────────────────┘", - output - ); - - Application.End (rsDialog); - Application.End (rsTop); - top.Dispose (); - } - - [Fact (Skip = "#3798 Broke. Will fix in #2975")] - [AutoInitShutdown] - public void Draw_A_Menu_Over_A_Top_Dialog () - { - ((FakeDriver)Application.Driver).SetBufferSize (40, 15); - - // Override CM - Window.DefaultBorderStyle = LineStyle.Single; - Dialog.DefaultButtonAlignment = Alignment.Center; - Dialog.DefaultBorderStyle = LineStyle.Single; - Dialog.DefaultShadow = ShadowStyle.None; - Button.DefaultShadow = ShadowStyle.None; - - Assert.Equal (new (0, 0, 40, 15), View.GetClip ()!.GetBounds()); - DriverAssert.AssertDriverContentsWithFrameAre (@"", output); - - List items = new () - { - "New", - "Open", - "Close", - "Save", - "Save As", - "Delete" - }; - var dialog = new Dialog { X = 2, Y = 2, Width = 15, Height = 4 }; - var menu = new MenuBar { X = Pos.Center (), Width = 10 }; - - menu.Menus = new MenuBarItem [] - { - new ( - "File", - new MenuItem [] - { - new ( - items [0], - "Create a new file", - () => ChangeMenuTitle ("New"), - null, - null, - KeyCode.CtrlMask | KeyCode.N - ), - new ( - items [1], - "Open a file", - () => ChangeMenuTitle ("Open"), - null, - null, - KeyCode.CtrlMask | KeyCode.O - ), - new ( - items [2], - "Close a file", - () => ChangeMenuTitle ("Close"), - null, - null, - KeyCode.CtrlMask | KeyCode.C - ), - new ( - items [3], - "Save a file", - () => ChangeMenuTitle ("Save"), - null, - null, - KeyCode.CtrlMask | KeyCode.S - ), - new ( - items [4], - "Save a file as", - () => ChangeMenuTitle ("Save As"), - null, - null, - KeyCode.CtrlMask | KeyCode.A - ), - new ( - items [5], - "Delete a file", - () => ChangeMenuTitle ("Delete"), - null, - null, - KeyCode.CtrlMask | KeyCode.A - ) - } - ) - }; - dialog.Add (menu); - - void ChangeMenuTitle (string title) - { - menu.Menus [0].Title = title; - menu.SetNeedsDraw (); - } - - RunState rs = Application.Begin (dialog); - Application.RunIteration (ref rs); - - Assert.Equal (new (2, 2, 15, 4), dialog.Frame); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌─────────────┐ - │ File │ - │ │ - └─────────────┘", - output - ); - - Assert.Equal ("File", menu.Menus [0].Title); - menu.OpenMenu (); - Application.RunIteration (ref rs); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌─────────────┐ - │ File │ - │ ┌──────────────────────────────────┐ - └─│ New Create a new file Ctrl+N │ - │ Open Open a file Ctrl+O │ - │ Close Close a file Ctrl+C │ - │ Save Save a file Ctrl+S │ - │ Save As Save a file as Ctrl+A │ - │ Delete Delete a file Ctrl+A │ - └──────────────────────────────────┘", - output - ); - - Application.RaiseMouseEvent (new () { ScreenPosition = new (20, 5), Flags = MouseFlags.Button1Clicked }); - - // Need to fool MainLoop into thinking it's running - Application.MainLoop.Running = true; - Application.RunIteration (ref rs); - Assert.Equal (items [0], menu.Menus [0].Title); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌─────────────┐ - │ New │ - │ │ - └─────────────┘", - output - ); - - for (var i = 1; i < items.Count; i++) - { - menu.OpenMenu (); - - Application.RaiseMouseEvent (new () { ScreenPosition = new (20, 5 + i), Flags = MouseFlags.Button1Clicked }); - - Application.RunIteration (ref rs); - Assert.Equal (items [i], menu.Menus [0].Title); - } - - ((FakeDriver)Application.Driver!).SetBufferSize (20, 15); - menu.OpenMenu (); - Application.RunIteration (ref rs); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - ┌─────────────┐ - │ Delete │ - │ ┌─────────────── - └─│ New Create - │ Open O - │ Close Cl - │ Save S - │ Save As Save - │ Delete Del - └───────────────", - output - ); + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); + + // Act + Application.RaiseKeyDownEvent (MenuBarv2.DefaultKey); + Assert.True (menuBar.IsActive ()); + Assert.True (menuBar.IsOpen ()); + Assert.True (menuBarItem.PopoverMenu.Visible); + + Application.RaiseKeyDownEvent (Application.QuitKey); + Assert.False (menuBar.IsActive ()); + Assert.False (menuBar.IsOpen ()); + Assert.False (menuBar.HasFocus); + Assert.False (menuBar.CanFocus); + Assert.False (menuBarItem.PopoverMenu.Visible); + Assert.False (menuBarItem.PopoverMenu.HasFocus); Application.End (rs); - dialog.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void DrawFrame_With_Negative_Positions () - { - var menu = new MenuBar - { - X = -1, - Y = -1, - Menus = - [ - new (new MenuItem [] { new ("One", "", null), new ("Two", "", null) }) - ] - }; - menu.Layout (); - - Assert.Equal (new (-1, -1), new Point (menu.Frame.X, menu.Frame.Y)); - - Toplevel top = new (); - Application.Begin (top); - menu.OpenMenu (); - Application.LayoutAndDraw (); - - var expected = @" -──────┐ - One │ - Two │ -──────┘ -"; - - Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (0, 0, 7, 4), pos); - - menu.CloseAllMenus (); - menu.Frame = new (-1, -2, menu.Frame.Width, menu.Frame.Height); - menu.OpenMenu (); - Application.LayoutAndDraw (); - - expected = @" - One │ - Two │ -──────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 7, 3), pos); - - menu.CloseAllMenus (); - menu.Frame = new (0, 0, menu.Frame.Width, menu.Frame.Height); - ((FakeDriver)Application.Driver!).SetBufferSize (7, 5); - menu.OpenMenu (); - Application.LayoutAndDraw (); - - expected = @" -┌────── -│ One -│ Two -└────── -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (0, 1, 7, 4), pos); - - menu.CloseAllMenus (); - menu.Frame = new (0, 0, menu.Frame.Width, menu.Frame.Height); - ((FakeDriver)Application.Driver!).SetBufferSize (7, 3); - menu.OpenMenu (); - Application.LayoutAndDraw (); - - expected = @" -┌────── -│ One -│ Two -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (0, 0, 7, 3), pos); top.Dispose (); } [Fact] [AutoInitShutdown] - public void DrawFrame_With_Negative_Positions_Disabled_Border () + public void MenuBarItem_HotKey_Activates () { - var menu = new MenuBar - { - X = -2, - Y = -1, - MenusBorderStyle = LineStyle.None, - Menus = - [ - new (new MenuItem [] { new ("One", "", null), new ("Two", "", null) }) - ] - }; - menu.Layout (); - - Assert.Equal (new (-2, -1), new Point (menu.Frame.X, menu.Frame.Y)); - - Toplevel top = new (); - Application.Begin (top); - menu.OpenMenu (); - Application.LayoutAndDraw (); - - var expected = @" -ne -wo -"; - - _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - - menu.CloseAllMenus (); - menu.Frame = new (-2, -2, menu.Frame.Width, menu.Frame.Height); - menu.OpenMenu (); - Application.LayoutAndDraw (); - - expected = @" -wo -"; - - _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - - menu.CloseAllMenus (); - menu.Frame = new (0, 0, menu.Frame.Width, menu.Frame.Height); - ((FakeDriver)Application.Driver!).SetBufferSize (3, 2); - menu.OpenMenu (); - Application.LayoutAndDraw (); - - expected = @" - On - Tw -"; - - _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - - menu.CloseAllMenus (); - menu.Frame = new (0, 0, menu.Frame.Width, menu.Frame.Height); - ((FakeDriver)Application.Driver!).SetBufferSize (3, 1); - menu.OpenMenu (); - Application.LayoutAndDraw (); - - expected = @" - On -"; - - _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void DrawFrame_With_Positive_Positions () - { - var menu = new MenuBar - { - Menus = - [ - new (new MenuItem [] { new ("One", "", null), new ("Two", "", null) }) - ] - }; - - Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); - - Toplevel top = new (); - Application.Begin (top); - menu.OpenMenu (); - Application.LayoutAndDraw (); - - var expected = @" -┌──────┐ -│ One │ -│ Two │ -└──────┘ -"; - - Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (0, 1, 8, 4), pos); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void DrawFrame_With_Positive_Positions_Disabled_Border () - { - var menu = new MenuBar - { - MenusBorderStyle = LineStyle.None, - Menus = - [ - new (new MenuItem [] { new ("One", "", null), new ("Two", "", null) }) - ] - }; - - Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); - - Toplevel top = new (); - Application.Begin (top); - menu.OpenMenu (); - Application.LayoutAndDraw (); - - var expected = @" - One - Two -"; - - _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - top.Dispose (); - } - - [Fact] - public void Exceptions () - { - Assert.Throws (() => new MenuBarItem ("Test", (MenuItem [])null)); - Assert.Throws (() => new MenuBarItem ("Test", (List)null)); - } - - [Fact] - [AutoInitShutdown] - public void HotKey_MenuBar_OnKeyDown_OnKeyUp_ProcessKeyPressed () - { - var newAction = false; - var copyAction = false; - - var menu = new MenuBar - { - Menus = - [ - new ("_File", new MenuItem [] { new ("_New", "", () => newAction = true) }), - new ( - "_Edit", - new MenuItem [] { new ("_Copy", "", () => copyAction = true) } - ) - ] - }; - - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.False (newAction); - Assert.False (copyAction); - -#if SUPPORT_ALT_TO_ACTIVATE_MENU - Assert.False (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); - Assert.False (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); - Assert.True (Application.Top.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); - Assert.True (menu.IsMenuOpen); - Application.Top.Draw (); - - string expected = @" - File Edit -"; - - var pos = DriverAsserts.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 11, 1), pos); - - Assert.True (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.N))); - Application.MainLoop.RunIteration (); - Assert.False (newAction); // not yet, hot keys don't work if the item is not visible - - Assert.True (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.F))); - Application.MainLoop.RunIteration (); - Assert.True (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.N))); - Application.MainLoop.RunIteration (); - Assert.True (newAction); - Application.Top.Draw (); - - expected = @" - File Edit -"; - - Assert.False (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); - Assert.True (Application.Top.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); - Assert.True (Application.Top.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); - Assert.True (menu.IsMenuOpen); - Application.Top.Draw (); - - expected = @" - File Edit -"; - - pos = DriverAsserts.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 11, 1), pos); - - Assert.True (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.CursorRight))); - Assert.True (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.C))); - Application.MainLoop.RunIteration (); - Assert.True (copyAction); -#endif - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void HotKey_MenuBar_ProcessKeyPressed_Menu_ProcessKey () - { - var newAction = false; - var copyAction = false; - - // Define the expected menu - var expectedMenu = new ExpectedMenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }), - new ( - "Edit", - new MenuItem [] { new ("Copy", "", null) } - ) - ] - }; - - // The real menu - var menu = new MenuBar - { - Menus = - [ - new ( - "_" + expectedMenu.Menus [0].Title, - new MenuItem [] - { - new ( - "_" + expectedMenu.Menus [0].Children [0].Title, - "", - () => newAction = true - ) - } - ), - new ( - "_" + expectedMenu.Menus [1].Title, - new MenuItem [] - { - new ( - "_" - + expectedMenu.Menus [1] - .Children [0] - .Title, - "", - () => copyAction = true - ) - } - ) - ] - }; - + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.False (newAction); - Assert.False (copyAction); - - Assert.True (menu.NewKeyDownEvent (Key.F.WithAlt)); - Assert.True (menu.IsMenuOpen); - Application.Top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - - Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.N)); - Application.MainLoop.RunIteration (); - Assert.True (newAction); + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); - Assert.True (menu.NewKeyDownEvent (Key.E.WithAlt)); - Assert.True (menu.IsMenuOpen); - Application.Top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); + // Act + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.True (menuBar.IsOpen ()); + Assert.True (menuBar.HasFocus); + Assert.True (menuBarItem.PopoverMenu.Visible); + Assert.True (menuBarItem.PopoverMenu.HasFocus); - Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.C)); - Application.MainLoop.RunIteration (); - Assert.True (copyAction); + Application.End (rs); top.Dispose (); } [Fact] [AutoInitShutdown] - public void Key_Open_And_Close_The_MenuBar () + public void MenuBarItem_HotKey_Deactivates () { - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }) - ] - }; + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.True (top.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - Assert.True (top.NewKeyDownEvent (menu.Key)); - Assert.False (menu.IsMenuOpen); - - menu.Key = Key.F10.WithShift; - Assert.False (top.NewKeyDownEvent (Key.F9)); - Assert.False (menu.IsMenuOpen); - - Assert.True (top.NewKeyDownEvent (Key.F10.WithShift)); - Assert.True (menu.IsMenuOpen); - Assert.True (top.NewKeyDownEvent (Key.F10.WithShift)); - Assert.False (menu.IsMenuOpen); - top.Dispose (); - } - - [Theory] - [AutoInitShutdown] - [InlineData ("_File", "_New", "", KeyCode.Space | KeyCode.CtrlMask)] - [InlineData ("Closed", "None", "", KeyCode.Space | KeyCode.CtrlMask, KeyCode.Space | KeyCode.CtrlMask)] - [InlineData ("_File", "_New", "", KeyCode.F9)] - [InlineData ("Closed", "None", "", KeyCode.F9, KeyCode.F9)] - [InlineData ("_File", "_Open", "", KeyCode.F9, KeyCode.CursorDown)] - [InlineData ("_File", "_Save", "", KeyCode.F9, KeyCode.CursorDown, KeyCode.CursorDown)] - [InlineData ("_File", "_Quit", "", KeyCode.F9, KeyCode.CursorDown, KeyCode.CursorDown, KeyCode.CursorDown)] - [InlineData ( - "_File", - "_New", - "", - KeyCode.F9, - KeyCode.CursorDown, - KeyCode.CursorDown, - KeyCode.CursorDown, - KeyCode.CursorDown - )] - [InlineData ("_File", "_New", "", KeyCode.F9, KeyCode.CursorDown, KeyCode.CursorUp)] - [InlineData ("_File", "_Quit", "", KeyCode.F9, KeyCode.CursorUp)] - [InlineData ("_File", "_New", "", KeyCode.F9, KeyCode.CursorUp, KeyCode.CursorDown)] - [InlineData ("Closed", "None", "Open", KeyCode.F9, KeyCode.CursorDown, KeyCode.Enter)] - [InlineData ("_Edit", "_Copy", "", KeyCode.F9, KeyCode.CursorRight)] - [InlineData ("_About", "_About", "", KeyCode.F9, KeyCode.CursorLeft)] - [InlineData ("_Edit", "_Copy", "", KeyCode.F9, KeyCode.CursorLeft, KeyCode.CursorLeft)] - [InlineData ("_Edit", "_Select All", "", KeyCode.F9, KeyCode.CursorRight, KeyCode.CursorUp)] - [InlineData ("_File", "_New", "", KeyCode.F9, KeyCode.CursorRight, KeyCode.CursorDown, KeyCode.CursorLeft)] - [InlineData ("_About", "_About", "", KeyCode.F9, KeyCode.CursorRight, KeyCode.CursorRight)] - [InlineData ("Closed", "None", "New", KeyCode.F9, KeyCode.Enter)] - [InlineData ("Closed", "None", "Quit", KeyCode.F9, KeyCode.CursorUp, KeyCode.Enter)] - [InlineData ("Closed", "None", "Copy", KeyCode.F9, KeyCode.CursorRight, KeyCode.Enter)] - [InlineData ( - "Closed", - "None", - "Find", - KeyCode.F9, - KeyCode.CursorRight, - KeyCode.CursorUp, - KeyCode.CursorUp, - KeyCode.Enter - )] - [InlineData ( - "Closed", - "None", - "Replace", - KeyCode.F9, - KeyCode.CursorRight, - KeyCode.CursorUp, - KeyCode.CursorUp, - KeyCode.CursorDown, - KeyCode.Enter - )] - [InlineData ( - "_Edit", - "F_ind", - "", - KeyCode.F9, - KeyCode.CursorRight, - KeyCode.CursorUp, - KeyCode.CursorUp, - KeyCode.CursorLeft, - KeyCode.Enter - )] - [InlineData ("Closed", "None", "About", KeyCode.F9, KeyCode.CursorRight, KeyCode.CursorRight, KeyCode.Enter)] - - //// Hotkeys - [InlineData ("_File", "_New", "", KeyCode.AltMask | KeyCode.F)] - [InlineData ("Closed", "None", "", KeyCode.AltMask | KeyCode.ShiftMask | KeyCode.F)] - [InlineData ("Closed", "None", "", KeyCode.AltMask | KeyCode.F, KeyCode.Esc)] - [InlineData ("Closed", "None", "", KeyCode.AltMask | KeyCode.F, KeyCode.AltMask | KeyCode.F)] - [InlineData ("Closed", "None", "Open", KeyCode.AltMask | KeyCode.F, KeyCode.O)] - [InlineData ("_File", "_New", "", KeyCode.AltMask | KeyCode.F, KeyCode.ShiftMask | KeyCode.O)] - [InlineData ("Closed", "None", "Open", KeyCode.AltMask | KeyCode.F, KeyCode.AltMask | KeyCode.O)] - [InlineData ("_Edit", "_Copy", "", KeyCode.AltMask | KeyCode.E)] - [InlineData ("_Edit", "F_ind", "", KeyCode.AltMask | KeyCode.E, KeyCode.F)] - [InlineData ("_Edit", "F_ind", "", KeyCode.AltMask | KeyCode.E, KeyCode.AltMask | KeyCode.F)] - [InlineData ("Closed", "None", "Replace", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.R)] - [InlineData ("Closed", "None", "Copy", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.C)] - [InlineData ("_Edit", "_1st", "", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3)] - [InlineData ("Closed", "None", "1", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3, KeyCode.D1)] - [InlineData ("Closed", "None", "1", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3, KeyCode.Enter)] - [InlineData ("Closed", "None", "2", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3, KeyCode.D2)] - [InlineData ("_Edit", "_5th", "", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3, KeyCode.D4)] - [InlineData ("Closed", "None", "5", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D4, KeyCode.D5)] - [InlineData ("Closed", "None", "About", KeyCode.AltMask | KeyCode.A)] - public void KeyBindings_Navigation_Commands ( - string expectedBarTitle, - string expectedItemTitle, - string expectedAction, - params KeyCode [] keys - ) - { - var miAction = ""; - MenuItem mbiCurrent = null; - MenuItem miCurrent = null; - - var menu = new MenuBar (); - - Func fn = s => - { - miAction = s as string; - - return true; - }; - menu.EnableForDesign (ref fn); - - menu.Key = KeyCode.F9; - menu.MenuOpening += (s, e) => mbiCurrent = e.CurrentMenu; - menu.MenuOpened += (s, e) => { miCurrent = e.MenuItem; }; - - menu.MenuClosing += (s, e) => - { - mbiCurrent = null; - miCurrent = null; - }; - menu.UseKeysUpDownAsKeysLeftRight = true; - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - foreach (Key key in keys) - { - top.NewKeyDownEvent (key); - Application.MainLoop.RunIteration (); - } + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); + + // Act + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.True (menuBar.IsActive ()); + Assert.True (menuBar.IsOpen ()); + Assert.True (menuBarItem.PopoverMenu.Visible); + + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.False (menuBar.IsActive ()); + Assert.False (menuBar.IsOpen ()); + Assert.False (menuBar.HasFocus); + Assert.False (menuBar.CanFocus); + Assert.False (menuBarItem.PopoverMenu.Visible); + Assert.False (menuBarItem.PopoverMenu.HasFocus); - Assert.Equal (expectedBarTitle, mbiCurrent != null ? mbiCurrent.Title : "Closed"); - Assert.Equal (expectedItemTitle, miCurrent != null ? miCurrent.Title : "None"); - Assert.Equal (expectedAction, miAction); + Application.End (rs); top.Dispose (); } - [Theory] - [AutoInitShutdown] - [InlineData ("New", KeyCode.CtrlMask | KeyCode.N)] - [InlineData ("Quit", KeyCode.CtrlMask | KeyCode.Q)] - [InlineData ("Copy", KeyCode.CtrlMask | KeyCode.C)] - [InlineData ("Replace", KeyCode.CtrlMask | KeyCode.H)] - [InlineData ("1", KeyCode.F1)] - [InlineData ("5", KeyCode.CtrlMask | KeyCode.D5)] - public void KeyBindings_Shortcut_Commands (string expectedAction, params KeyCode [] keys) - { - var miAction = ""; - MenuItem mbiCurrent = null; - MenuItem miCurrent = null; - - var menu = new MenuBar (); - - bool FnAction (string s) - { - miAction = s; - - return true; - } - - // Declare a variable for the function - Func fnActionVariable = FnAction; - - menu.EnableForDesign (ref fnActionVariable); - - menu.Key = KeyCode.F9; - menu.MenuOpening += (s, e) => mbiCurrent = e.CurrentMenu; - menu.MenuOpened += (s, e) => { miCurrent = e.MenuItem; }; - - menu.MenuClosing += (s, e) => - { - mbiCurrent = null; - miCurrent = null; - }; - menu.UseKeysUpDownAsKeysLeftRight = true; - - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - foreach (KeyCode key in keys) - { - Assert.True (top.NewKeyDownEvent (new (key))); - Application.MainLoop!.RunIteration (); - } - - Assert.Equal (expectedAction, miAction); - top.Dispose (); - } [Fact] [AutoInitShutdown] - public void Menu_With_Separator () + public void MenuItem_HotKey_Deactivates () { - var menu = new MenuBar - { - Menus = - [ - new ( - "File", - new MenuItem [] - { - new ( - "_Open", - "Open a file", - () => { }, - null, - null, - KeyCode.CtrlMask | KeyCode.O - ), - null, - new ("_Quit", "", null) - } - ) - ] - }; - + // Arrange + int action = 0; + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item", Action = () => action++ }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - menu.OpenMenu (); - Application.LayoutAndDraw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - File -┌────────────────────────────┐ -│ Open Open a file Ctrl+O │ -├────────────────────────────┤ -│ Quit │ -└────────────────────────────┘", - output - ); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void Menu_With_Separator_Disabled_Border () - { - var menu = new MenuBar - { - MenusBorderStyle = LineStyle.None, - Menus = - [ - new ( - "File", - new MenuItem [] - { - new ( - "_Open", - "Open a file", - () => { }, - null, - null, - KeyCode.CtrlMask | KeyCode.O - ), - null, - new ("_Quit", "", null) - } - ) - ] - }; + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); + + // Act + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.True (menuBar.IsActive ()); + Assert.True (menuBarItem.PopoverMenu.Visible); + + Application.RaiseKeyDownEvent (Key.I); + Assert.Equal (1, action); + Assert.False (menuBar.IsActive ()); + Assert.False (menuBar.IsOpen ()); + Assert.False (menuBar.HasFocus); + Assert.False (menuBar.CanFocus); + Assert.False (menuBarItem.PopoverMenu.Visible); + Assert.False (menuBarItem.PopoverMenu.HasFocus); - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - menu.OpenMenu (); - Application.LayoutAndDraw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - File - Open Open a file Ctrl+O -──────────────────────────── - Quit ", - output - ); + Application.End (rs); top.Dispose (); } [Fact] [AutoInitShutdown] - public void MenuBar_ButtonPressed_Open_The_Menu_ButtonPressed_Again_Close_The_Menu () + public void HotKey_Activates_Only_Once () { - // Define the expected menu - var expectedMenu = new ExpectedMenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("Open", "", null) }), - new ( - "Edit", - new MenuItem [] { new ("Copy", "", null) } - ) - ] - }; - - // Test without HotKeys first - var menu = new MenuBar - { - Menus = - [ - new ( - "_" + expectedMenu.Menus [0].Title, - new MenuItem [] { new ("_" + expectedMenu.Menus [0].Children [0].Title, "", null) } - ), - new ( - "_" + expectedMenu.Menus [1].Title, - new MenuItem [] - { - new ( - "_" - + expectedMenu.Menus [1] - .Children [0] - .Title, - "", - null - ) - } - ) - ] - }; - + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.True (menu.NewMouseEvent (new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu })); - Assert.True (menu.IsMenuOpen); - top.Draw (); - - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - - Assert.True (menu.NewMouseEvent (new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu })); - Assert.False (menu.IsMenuOpen); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); - top.Dispose (); - } + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); - [Fact] - [AutoInitShutdown] - public void MenuBar_In_Window_Without_Other_Views_With_Top_Init () - { - var win = new Window (); + int visibleChangeCount = 0; + menuBarItemPopover.VisibleChanged += (sender, args) => + { + if (menuBarItemPopover.Visible) + { + visibleChangeCount++; + } + }; - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }), - new ( - "Edit", - new MenuItem [] - { - new MenuBarItem ( - "Delete", - new MenuItem [] - { new ("All", "", null), new ("Selected", "", null) } - ) - } - ) - ] - }; - win.Add (menu); - Toplevel top = new (); - top.Add (win); - Application.Begin (top); - ((FakeDriver)Application.Driver!).SetBufferSize (40, 8); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ │ -│ │ -│ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (win.NewKeyDownEvent (menu.Key)); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│┌──────┐ │ -││ New │ │ -│└──────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu.NewKeyDownEvent (Key.CursorRight)); - Application.LayoutAndDraw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ ┌─────────┐ │ -│ │ Delete ►│ │ -│ └─────────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ ┌─────────┐ │ -│ │ Delete ►│┌───────────┐ │ -│ └─────────┘│ All │ │ -│ │ Selected │ │ -│ └───────────┘ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); - View.SetClipToScreen (); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│┌──────┐ │ -││ New │ │ -│└──────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - top.Dispose (); - } + // Act + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.Equal (1, visibleChangeCount); - [Fact] - [AutoInitShutdown] - public void MenuBar_In_Window_Without_Other_Views_With_Top_Init_With_Parameterless_Run () - { - var win = new Window (); - - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }), - new ( - "Edit", - new MenuItem [] - { - new MenuBarItem ( - "Delete", - new MenuItem [] - { new ("All", "", null), new ("Selected", "", null) } - ) - } - ) - ] - }; - win.Add (menu); - Toplevel top = new (); - top.Add (win); - - Application.Iteration += (s, a) => - { - ((FakeDriver)Application.Driver!).SetBufferSize (40, 8); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ │ -│ │ -│ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (win.NewKeyDownEvent (menu.Key)); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│┌──────┐ │ -││ New │ │ -│└──────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu.NewKeyDownEvent (Key.CursorRight)); - Application.LayoutAndDraw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ ┌─────────┐ │ -│ │ Delete ►│ │ -│ └─────────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ ┌─────────┐ │ -│ │ Delete ►│┌───────────┐ │ -│ └─────────┘│ All │ │ -│ │ Selected │ │ -│ └───────────┘ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); - View.SetClipToScreen (); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│┌──────┐ │ -││ New │ │ -│└──────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Application.RequestStop (); - }; - - Application.Run (top); + Application.End (rs); top.Dispose (); } [Fact] [AutoInitShutdown] - public void MenuBar_In_Window_Without_Other_Views_Without_Top_Init () - { - var win = new Window (); - - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }), - new ( - "Edit", - new MenuItem [] - { - new MenuBarItem ( - "Delete", - new MenuItem [] - { new ("All", "", null), new ("Selected", "", null) } - ) - } - ) - ] - }; - win.Add (menu); - ((FakeDriver)Application.Driver!).SetBufferSize (40, 8); - RunState rs = Application.Begin (win); - Application.RunIteration (ref rs); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ │ -│ │ -│ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (win.NewKeyDownEvent (menu.Key)); - Application.RunIteration (ref rs); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│┌──────┐ │ -││ New │ │ -│└──────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu.NewKeyDownEvent (Key.CursorRight)); - Application.RunIteration (ref rs); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ ┌─────────┐ │ -│ │ Delete ►│ │ -│ └─────────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); - Application.RunIteration (ref rs); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ ┌─────────┐ │ -│ │ Delete ►│┌───────────┐ │ -│ └─────────┘│ All │ │ -│ │ Selected │ │ -│ └───────────┘ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); - Application.RunIteration (ref rs); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│┌──────┐ │ -││ New │ │ -│└──────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - win.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void MenuBar_In_Window_Without_Other_Views_Without_Top_Init_With_Run_T () + public void WhenActive_Other_MenuBarItem_HotKey_Activates () { - ((FakeDriver)Application.Driver!).SetBufferSize (40, 8); - - Application.Iteration += (s, a) => - { - Toplevel top = Application.Top; - Application.LayoutAndDraw(); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ │ -│ │ -│ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (top.NewKeyDownEvent (Key.F9)); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│┌──────┐ │ -││ New │ │ -│└──────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True (top.SubViews.ElementAt (0).NewKeyDownEvent (Key.CursorRight)); - Application.LayoutAndDraw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ ┌─────────┐ │ -│ │ Delete ►│ │ -│ └─────────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True ( - ((MenuBar)top.SubViews.ElementAt (0))._openMenu.NewKeyDownEvent (Key.CursorRight) - ); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│ ┌─────────┐ │ -│ │ Delete ►│┌───────────┐ │ -│ └─────────┘│ All │ │ -│ │ Selected │ │ -│ └───────────┘ │ -└──────────────────────────────────────┘", - output - ); - - Assert.True ( - ((MenuBar)top.SubViews.ElementAt (0))._openMenu.NewKeyDownEvent (Key.CursorRight) - ); - View.SetClipToScreen (); - top.Draw (); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" -┌──────────────────────────────────────┐ -│ File Edit │ -│┌──────┐ │ -││ New │ │ -│└──────┘ │ -│ │ -│ │ -└──────────────────────────────────────┘", - output - ); - - Application.RequestStop (); - }; - - Application.Run ().Dispose (); - } + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; - [Fact] - [AutoInitShutdown] - public void MenuBar_Position_And_Size_With_HotKeys_Is_The_Same_As_Without_HotKeys () - { - // Define the expected menu - var expectedMenu = new ExpectedMenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("12", "", null) }), - new ( - "Edit", - new MenuItem [] { new ("Copy", "", null) } - ) - ] - }; + var menuItem2 = new MenuItemv2 { Id = "menuItem2", Title = "_Copy" }; + var menu2 = new Menuv2 ([menuItem2]) { Id = "menu2" }; + var menuBarItem2 = new MenuBarItemv2 () { Id = "menuBarItem2", Title = "_Edit" }; + var menuBarItemPopover2 = new PopoverMenu () { Id = "menuBarItemPopover2" }; + menuBarItem2.PopoverMenu = menuBarItemPopover2; + menuBarItemPopover2.Root = menu2; - // Test without HotKeys first - var menu = new MenuBar - { - Menus = - [ - new ( - expectedMenu.Menus [0].Title, - new MenuItem [] { new (expectedMenu.Menus [0].Children [0].Title, "", null) } - ), - new ( - expectedMenu.Menus [1].Title, - new MenuItem [] - { - new ( - expectedMenu.Menus [1].Children [0].Title, - "", - null - ) - } - ) - ] - }; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + menuBar.Add (menuBarItem2); var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - // Open first - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - - // Open second - Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorRight)); - Assert.True (menu.IsMenuOpen); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); - - // Close menu - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.False (menu.IsMenuOpen); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); - - top.Remove (menu); - - // Now test WITH HotKeys - menu = new () - { - Menus = - [ - new ( - "_" + expectedMenu.Menus [0].Title, - new MenuItem [] { new ("_" + expectedMenu.Menus [0].Children [0].Title, "", null) } - ), - new ( - "_" + expectedMenu.Menus [1].Title, - new MenuItem [] - { - new ( - "_" + expectedMenu.Menus [1].Children [0].Title, - "", - null - ) - } - ) - ] - }; - - top.Add (menu); - - // Open first - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - - // Open second - Assert.True (top.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorRight)); - Assert.True (menu.IsMenuOpen); - View.SetClipToScreen (); - Application.Top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); - - // Close menu - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.False (menu.IsMenuOpen); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void MenuBar_Submenus_Alignment_Correct () - { - // Define the expected menu - var expectedMenu = new ExpectedMenuBar - { - Menus = - [ - new ( - "File", - new MenuItem [] - { - new ( - "Really Long Sub Menu", - "", - null - ) - } - ), - new ( - "123", - new MenuItem [] { new ("Copy", "", null) } - ), - new ( - "Format", - new MenuItem [] { new ("Word Wrap", "", null) } - ), - new ( - "Help", - new MenuItem [] { new ("About", "", null) } - ), - new ( - "1", - new MenuItem [] { new ("2", "", null) } - ), - new ( - "3", - new MenuItem [] { new ("2", "", null) } - ), - new ( - "Last one", - new MenuItem [] { new ("Test", "", null) } - ) - ] - }; - - MenuBarItem [] items = new MenuBarItem [expectedMenu.Menus.Length]; - - for (var i = 0; i < expectedMenu.Menus.Length; i++) - { - items [i] = new ( - expectedMenu.Menus [i].Title, - new MenuItem [] { new (expectedMenu.Menus [i].Children [0].Title, "", null) } - ); - } - - var menu = new MenuBar { Menus = items }; + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); + // Act + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.True (menuBar.IsActive ()); + Assert.True (menuBar.IsOpen ()); + Assert.True (menuBarItem.PopoverMenu.Visible); - for (var i = 0; i < expectedMenu.Menus.Length; i++) - { - menu.OpenMenu (i); - Assert.True (menu.IsMenuOpen); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (i), output); - } + Application.RaiseKeyDownEvent (Key.E.WithAlt); + Assert.True (menuBar.IsActive ()); + Assert.True (menuBar.IsOpen ()); + Assert.True (menuBarItem2.PopoverMenu.Visible); + Assert.False (menuBarItem.PopoverMenu.Visible); + Application.End (rs); top.Dispose (); } [Fact] [AutoInitShutdown] - public void MenuBar_With_Action_But_Without_MenuItems_Not_Throw () + public void Mouse_Enter_Sets_Can_Focus_But_Does_Not_Activate () { - var menu = new MenuBar - { - Menus = - [ - new () { Title = "Test 1", Action = () => { } }, - - new () { Title = "Test 2", Action = () => { } } - ] - }; - + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - -#if SUPPORT_ALT_TO_ACTIVATE_MENU - Assert.True ( - Application.OnKeyUp ( - new KeyEventArgs ( - Key.AltMask - ) - ) - ); // changed to true because Alt activates menu bar -#endif - Assert.True (menu.NewKeyDownEvent (Key.CursorRight)); - Assert.True (menu.NewKeyDownEvent (Key.CursorRight)); - top.Dispose (); - } + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); - [Fact] - [AutoInitShutdown] - public void MenuBarItem_Children_Null_Does_Not_Throw () - { - var menu = new MenuBar + // Act + Application.RaiseMouseEvent (new () { - Menus = - [ - new ("Test", "", null) - ] - }; - var top = new Toplevel (); - top.Add (menu); + Flags = MouseFlags.ReportMousePosition + }); + Assert.False (menuBar.IsActive ()); + Assert.False (menuBar.IsOpen ()); + Assert.True (menuBar.HasFocus); + Assert.True (menuBar.CanFocus); + Assert.False (menuBarItem.PopoverMenu.Visible); + Assert.False (menuBarItem.PopoverMenu.HasFocus); - Exception exception = Record.Exception (() => menu.NewKeyDownEvent (Key.Space)); - Assert.Null (exception); + Application.End (rs); top.Dispose (); } [Fact] [AutoInitShutdown] - public void MenuOpened_On_Disabled_MenuItem () + public void Mouse_Click_Activates () { - MenuItem parent = null; - MenuItem miCurrent = null; - Menu mCurrent = null; - - var menu = new MenuBar - { - Menus = - [ - new ( - "_File", - new MenuItem [] - { - new MenuBarItem ( - "_New", - new MenuItem [] - { - new ( - "_New doc", - "Creates new doc.", - null, - () => false - ) - } - ), - null, - new ("_Save", "Saves the file.", null) - } - ) - ] - }; - - menu.MenuOpened += (s, e) => - { - parent = e.Parent; - miCurrent = e.MenuItem; - mCurrent = menu._openMenu; - }; - menu.UseKeysUpDownAsKeysLeftRight = true; + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - // open the menu - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } - ) - ); - Assert.True (menu.IsMenuOpen); - Assert.Equal ("_File", parent.Title); - Assert.Equal ("_File", miCurrent.Parent.Title); - Assert.Equal ("_New", miCurrent.Title); - - Assert.True ( - mCurrent.NewMouseEvent ( - new () { Position = new (1, 1), Flags = MouseFlags.ReportMousePosition, View = mCurrent } - ) - ); - Assert.True (menu.IsMenuOpen); - Assert.Equal ("_File", parent.Title); - Assert.Equal ("_File", miCurrent.Parent.Title); - Assert.Equal ("_New", miCurrent.Title); - - Assert.True ( - mCurrent.NewMouseEvent ( - new () { Position = new (1, 1), Flags = MouseFlags.ReportMousePosition, View = mCurrent } - ) - ); - Assert.True (menu.IsMenuOpen); - Assert.Equal ("_File", parent.Title); - Assert.Equal ("_File", miCurrent.Parent.Title); - Assert.Equal ("_New", miCurrent.Title); - - Assert.True ( - mCurrent.NewMouseEvent ( - new () { Position = new (1, 2), Flags = MouseFlags.ReportMousePosition, View = mCurrent } - ) - ); - Assert.True (menu.IsMenuOpen); - Assert.Equal ("_File", parent.Title); - Assert.Equal ("_File", miCurrent.Parent.Title); - Assert.Equal ("_Save", miCurrent.Title); - - // close the menu - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } - ) - ); - Assert.False (menu.IsMenuOpen); - - // open the menu - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - - // The _New doc is enabled but the sub-menu isn't enabled. Is show but can't be selected and executed - Assert.Equal ("_New", parent.Title); - Assert.Equal ("_New", miCurrent.Parent.Title); - Assert.Equal ("_New doc", miCurrent.Title); - - Assert.True (mCurrent.NewKeyDownEvent (Key.CursorDown)); - Assert.True (menu.IsMenuOpen); - Assert.Equal ("_File", parent.Title); - Assert.Equal ("_File", miCurrent.Parent.Title); - Assert.Equal ("_Save", miCurrent.Title); - - Assert.True (mCurrent.NewKeyDownEvent (Key.CursorUp)); - Assert.True (menu.IsMenuOpen); - Assert.Equal ("_File", parent.Title); - Assert.Null (miCurrent); - - // close the menu - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.False (menu.IsMenuOpen); - top.Dispose (); - } - - [Fact] - [AutoInitShutdown] - public void MenuOpening_MenuOpened_MenuClosing_Events () - { - var miAction = ""; - var isMenuClosed = true; - var cancelClosing = false; + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); - var menu = new MenuBar + // Act + Application.RaiseMouseEvent (new () { - Menus = - [ - new ("_File", new MenuItem [] { new ("_New", "Creates new file.", New) }) - ] - }; - - menu.MenuOpening += (s, e) => - { - Assert.Equal ("_File", e.CurrentMenu.Title); - Assert.Equal ("_New", e.CurrentMenu.Children [0].Title); - Assert.Equal ("Creates new file.", e.CurrentMenu.Children [0].Help); - Assert.Equal (New, e.CurrentMenu.Children [0].Action); - e.CurrentMenu.Children [0].Action (); - Assert.Equal ("New", miAction); - - e.NewMenuBarItem = new ( - "_Edit", - new MenuItem [] { new ("_Copy", "Copies the selection.", Copy) } - ); - }; - - menu.MenuOpened += (s, e) => - { - MenuItem mi = e.MenuItem; - - Assert.Equal ("_Edit", mi.Parent.Title); - Assert.Equal ("_Copy", mi.Title); - Assert.Equal ("Copies the selection.", mi.Help); - Assert.Equal (Copy, mi.Action); - mi.Action (); - Assert.Equal ("Copy", miAction); - }; - - menu.MenuClosing += (s, e) => - { - Assert.False (isMenuClosed); - - if (cancelClosing) - { - e.Cancel = true; - isMenuClosed = false; - } - else - { - isMenuClosed = true; - } - }; - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - isMenuClosed = !menu.IsMenuOpen; - Assert.False (isMenuClosed); - top.Draw (); - - var expected = @" -Edit -┌──────────────────────────────┐ -│ Copy Copies the selection. │ -└──────────────────────────────┘ -"; - DriverAssert.AssertDriverContentsAre (expected, output); - - cancelClosing = true; - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - Assert.False (isMenuClosed); - View.SetClipToScreen (); - top.Draw (); - - expected = @" -Edit -┌──────────────────────────────┐ -│ Copy Copies the selection. │ -└──────────────────────────────┘ -"; - DriverAssert.AssertDriverContentsAre (expected, output); - - cancelClosing = false; - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.False (menu.IsMenuOpen); - Assert.True (isMenuClosed); - View.SetClipToScreen (); - top.Draw (); - - expected = @" -Edit -"; - DriverAssert.AssertDriverContentsAre (expected, output); - - void New () { miAction = "New"; } - - void Copy () { miAction = "Copy"; } + Flags = MouseFlags.Button1Clicked + }); + Assert.True (menuBar.IsActive ()); + Assert.True (menuBar.IsOpen ()); + Assert.True (menuBar.HasFocus); + Assert.True (menuBar.CanFocus); + Assert.True (menuBarItem.PopoverMenu.Visible); + Assert.True (menuBarItem.PopoverMenu.HasFocus); + Application.End (rs); top.Dispose (); } - [Fact] + // QUESTION: Windows' menus close the menu when you click on the menu bar item again. + // QUESTION: What does Mac do? + // QUESTION: How bad is it that this test is skipped? + // QUESTION: Fixing this could be challenging. Should we fix it? + [Fact (Skip = "Clicking outside Popover, passes mouse event to MenuBar, which activates the same item again.")] [AutoInitShutdown] - public void MouseEvent_Test () + public void Mouse_Click_Deactivates () { - MenuItem miCurrent = null; - Menu mCurrent = null; - - var menuBar = new MenuBar - { - Menus = - [ - new ( - "_File", - new MenuItem [] { new ("_New", "", null), new ("_Open", "", null), new ("_Save", "", null) } - ), - new ( - "_Edit", - new MenuItem [] { new ("_Copy", "", null), new ("C_ut", "", null), new ("_Paste", "", null) } - ) - ] - }; - - menuBar.MenuOpened += (s, e) => - { - miCurrent = e.MenuItem; - mCurrent = menuBar.OpenCurrentMenu; - }; + // Arrange + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); var top = new Toplevel (); top.Add (menuBar); - Application.Begin (top); - - // Click on Edit - Assert.True ( - menuBar.NewMouseEvent ( - new () { Position = new (10, 0), Flags = MouseFlags.Button1Pressed, View = menuBar } - ) - ); - Assert.True (menuBar.IsMenuOpen); - Assert.Equal ("_Edit", miCurrent.Parent.Title); - Assert.Equal ("_Copy", miCurrent.Title); - - // Click on Paste - Assert.True ( - mCurrent.NewMouseEvent ( - new () { Position = new (10, 2), Flags = MouseFlags.ReportMousePosition, View = mCurrent } - ) - ); - Assert.True (menuBar.IsMenuOpen); - Assert.Equal ("_Edit", miCurrent.Parent.Title); - Assert.Equal ("_Paste", miCurrent.Title); - - for (var i = 4; i >= -1; i--) - { - Application.RaiseMouseEvent ( - new () { ScreenPosition = new (10, i), Flags = MouseFlags.ReportMousePosition } - ); - - Assert.True (menuBar.IsMenuOpen); - Menu menu = (Menu)top.SubViews.First (v => v is Menu); - - if (i is < 0 or > 0) - { - Assert.Equal (menu, Application.MouseGrabView); - } - else - { - Assert.Equal (menuBar, Application.MouseGrabView); - } - - Assert.Equal ("_Edit", miCurrent.Parent.Title); - - if (i == 4) - { - Assert.Equal ("_Paste", miCurrent.Title); - } - else if (i == 3) - { - Assert.Equal ("C_ut", miCurrent.Title); - } - else if (i == 2) - { - Assert.Equal ("_Copy", miCurrent.Title); - } - else - { - Assert.Equal ("_Copy", miCurrent.Title); - } - } + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); + + Application.RaiseMouseEvent (new () + { + Flags = MouseFlags.Button1Clicked + }); + Assert.True (menuBar.IsOpen ()); + Assert.True (menuBar.HasFocus); + Assert.True (menuBar.CanFocus); + Assert.True (menuBarItem.PopoverMenu.Visible); + Assert.True (menuBarItem.PopoverMenu.HasFocus); + + // Act + Application.RaiseMouseEvent (new () + { + Flags = MouseFlags.Button1Clicked + }); + Assert.False (menuBar.IsActive ()); + Assert.False (menuBar.IsOpen ()); + Assert.False (menuBar.HasFocus); + Assert.False (menuBar.CanFocus); + Assert.False (menuBarItem.PopoverMenu.Visible); + Assert.False (menuBarItem.PopoverMenu.HasFocus); + Application.End (rs); top.Dispose (); } [Fact] [AutoInitShutdown] - public void Parent_MenuItem_Stay_Focused_If_Child_MenuItem_Is_Empty_By_Keyboard () + public void Dynamic_Change_MenuItem_Title () { - var expectedMenu = new ExpectedMenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }), - new ("Edit", Array.Empty ()), - new ( - "Format", - new MenuItem [] { new ("Wrap", "", null) } - ) - ] - }; - - MenuBarItem [] items = new MenuBarItem [expectedMenu.Menus.Length]; - - for (var i = 0; i < expectedMenu.Menus.Length; i++) - { - items [i] = new ( - expectedMenu.Menus [i].Title, - expectedMenu.Menus [i].Children.Length > 0 - ? new MenuItem [] { new (expectedMenu.Menus [i].Children [0].Title, "", null) } - : Array.Empty () - ); - } - - var menu = new MenuBar { Menus = items }; - - var tf = new TextField { Y = 2, Width = 10 }; + // Arrange + int action = 0; + var menuItem = new MenuItemv2 { Title = "_Item", Action = () => action++ }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 (); + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); var top = new Toplevel (); - top.Add (menu, tf); - - Application.Begin (top); - Assert.True (tf.HasFocus); - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - - // Right - Edit has no sub menu; this tests that no sub menu shows - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - Assert.Equal (1, menu._selected); - Assert.Equal (-1, menu._selectedSub); - Assert.Null (menu._openSubMenu); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); - - // Right - Format - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (2), output); - - // Left - Edit - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorLeft)); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); - - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorLeft)); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - - Assert.True (Application.RaiseKeyDownEvent (menu.Key)); - Assert.False (menu.IsMenuOpen); - Assert.True (tf.HasFocus); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); - top.Dispose (); - } + top.Add (menuBar); + RunState rs = Application.Begin (top); - [Fact] - [AutoInitShutdown] - public void Parent_MenuItem_Stay_Focused_If_Child_MenuItem_Is_Empty_By_Mouse () - { - // File Edit Format - //┌──────┐ ┌───────┐ - //│ New │ │ Wrap │ - //└──────┘ └───────┘ + Assert.False (menuBar.IsActive()); + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.Equal (0, action); - // Define the expected menu - var expectedMenu = new ExpectedMenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }), - new ("Edit", new MenuItem [] { }), - new ( - "Format", - new MenuItem [] { new ("Wrap", "", null) } - ) - ] - }; + Assert.Equal(Key.I, menuItem.HotKey); + Application.RaiseKeyDownEvent (Key.I); + Assert.Equal (1, action); + Assert.False (menuBar.IsActive ()); - var menu = new MenuBar - { - Menus = - [ - new ( - expectedMenu.Menus [0].Title, - new MenuItem [] { new (expectedMenu.Menus [0].Children [0].Title, "", null) } - ), - new (expectedMenu.Menus [1].Title, new MenuItem [] { }), - new ( - expectedMenu.Menus [2].Title, - new MenuItem [] - { - new ( - expectedMenu.Menus [2].Children [0].Title, - "", - null - ) - } - ) - ] - }; + menuItem.Title = "_Foo"; + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.True (menuBar.IsActive ()); + Application.RaiseKeyDownEvent (Key.I); + Assert.Equal (1, action); + Assert.True (menuBar.IsActive ()); - var tf = new TextField { Y = 2, Width = 10 }; - var top = new Toplevel (); - top.Add (menu, tf); - Application.Begin (top); - - Assert.True (tf.HasFocus); - Assert.True (menu.NewMouseEvent (new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu })); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (8, 0), Flags = MouseFlags.ReportMousePosition, View = menu } - ) - ); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); - - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (15, 0), Flags = MouseFlags.ReportMousePosition, View = menu } - ) - ); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (2), output); - - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (8, 0), Flags = MouseFlags.ReportMousePosition, View = menu } - ) - ); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); - - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (1, 0), Flags = MouseFlags.ReportMousePosition, View = menu } - ) - ); - Assert.True (menu.IsMenuOpen); - Assert.False (tf.HasFocus); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); - - Assert.True (menu.NewMouseEvent (new () { Position = new (8, 0), Flags = MouseFlags.Button1Pressed, View = menu })); - Assert.False (menu.IsMenuOpen); - Assert.True (tf.HasFocus); - View.SetClipToScreen (); - top.Draw (); - DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); - top.Dispose (); - } + Application.RaiseKeyDownEvent (Key.F); + Assert.Equal (2, action); - [Fact] - public void RemoveAndThenAddMenuBar_ShouldNotChangeWidth () - { - MenuBar menuBar; - MenuBar menuBar2; - - // TODO: When https: //github.com/gui-cs/Terminal.Gui/issues/3136 is fixed, - // TODO: Change this to Window - var w = new View (); - menuBar2 = new (); - menuBar = new (); - w.Width = Dim.Fill (); - w.Height = Dim.Fill (); - w.X = 0; - w.Y = 0; - - w.Visible = true; - - // TODO: When https: //github.com/gui-cs/Terminal.Gui/issues/3136 is fixed, - // TODO: uncomment this. - //w.Modal = false; - w.Title = ""; - menuBar.Width = Dim.Fill (); - menuBar.Height = 1; - menuBar.X = 0; - menuBar.Y = 0; - menuBar.Visible = true; - w.Add (menuBar); - - menuBar2.Width = Dim.Fill (); - menuBar2.Height = 1; - menuBar2.X = 0; - menuBar2.Y = 4; - menuBar2.Visible = true; - w.Add (menuBar2); - - MenuBar [] menuBars = w.SubViews.OfType ().ToArray (); - Assert.Equal (2, menuBars.Length); - - Assert.Equal (Dim.Fill (), menuBars [0].Width); - Assert.Equal (Dim.Fill (), menuBars [1].Width); - - // Goes wrong here - w.Remove (menuBar); - w.Remove (menuBar2); - - w.Add (menuBar); - w.Add (menuBar2); - - // These assertions fail - Assert.Equal (Dim.Fill (), menuBars [0].Width); - Assert.Equal (Dim.Fill (), menuBars [1].Width); + Application.End (rs); + top.Dispose (); } [Fact] - [AutoInitShutdown] - public void Resizing_Close_Menus () - { - var menu = new MenuBar - { - Menus = - [ - new ( - "File", - new MenuItem [] - { - new ( - "Open", - "Open a file", - () => { }, - null, - null, - KeyCode.CtrlMask | KeyCode.O - ) - } - ) - ] - }; + [AutoInitShutdown (configLocation: ConfigLocations.Default)] + public void Disabled_MenuBar_Is_Not_Activated () + { + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); var top = new Toplevel (); - top.Add (menu); + top.Add (menuBar); RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); - menu.OpenMenu (); - var firstIteration = false; - Application.RunIteration (ref rs, firstIteration); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - File -┌────────────────────────────┐ -│ Open Open a file Ctrl+O │ -└────────────────────────────┘", - output - ); - - ((FakeDriver)Application.Driver!).SetBufferSize (20, 15); - firstIteration = false; - Application.RunIteration (ref rs, firstIteration); - - DriverAssert.AssertDriverContentsWithFrameAre ( - @" - File", - output - ); + // Act + menuBar.Enabled = false; + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.False (menuBar.IsActive ()); + Assert.False (menuBar.IsOpen ()); + Assert.False (menuBarItem.PopoverMenu.Visible); Application.End (rs); top.Dispose (); } [Fact] - public void Separator_Does_Not_Throws_Pressing_Menu_Hotkey () - { - var menu = new MenuBar - { - Menus = - [ - new ( - "File", - new MenuItem [] { new ("_New", "", null), null, new ("_Quit", "", null) } - ) - ] - }; - Assert.False (menu.NewKeyDownEvent (Key.Q.WithAlt)); - } - - [Fact] - public void SetMenus_With_Same_HotKey_Does_Not_Throws () - { - var mb = new MenuBar (); - - var i1 = new MenuBarItem ("_heey", "fff", () => { }, () => true); - - mb.Menus = new [] { i1 }; - mb.Menus = new [] { i1 }; - - Assert.Equal (Key.H, mb.Menus [0].HotKey); - } - - [Fact] - [AutoInitShutdown] - public void ShortCut_Activates () - { - var saveAction = false; - - var menu = new MenuBar - { - Menus = - [ - new ( - "_File", - new MenuItem [] - { - new ( - "_Save", - "Saves the file.", - () => { saveAction = true; }, - null, - null, - (KeyCode)Key.S.WithCtrl - ) - } - ) - ] - }; - + [AutoInitShutdown (configLocation: ConfigLocations.Default)] + public void Disabled_MenuBarItem_Is_Not_Activated () + { + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); - Application.RaiseKeyDownEvent (Key.S.WithCtrl); - Application.MainLoop.RunIteration (); + // Act + menuBarItem.Enabled = false; + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.False (menuBar.IsActive ()); + Assert.False (menuBar.IsOpen ()); + Assert.False (menuBarItem.PopoverMenu.Visible); - Assert.True (saveAction); + Application.End (rs); top.Dispose (); } - [Fact] - public void Update_ShortcutKey_KeyBindings_Old_ShortcutKey_Is_Removed () - { - var menuBar = new MenuBar - { - Menus = - [ - new ( - "_File", - new MenuItem [] - { - new ("New", "Create New", null, null, null, Key.A.WithCtrl) - } - ) - ] - }; - - Assert.True (menuBar.HotKeyBindings.TryGet (Key.A.WithCtrl, out _)); - - menuBar.Menus [0].Children! [0].ShortcutKey = Key.B.WithCtrl; - - Assert.False (menuBar.HotKeyBindings.TryGet (Key.A.WithCtrl, out _)); - Assert.True (menuBar.HotKeyBindings.TryGet (Key.B.WithCtrl, out _)); - } [Fact] - public void UseKeysUpDownAsKeysLeftRight_And_UseSubMenusSingleFrame_Cannot_Be_Both_True () - { - var menu = new MenuBar (); - Assert.False (menu.UseKeysUpDownAsKeysLeftRight); - Assert.False (menu.UseSubMenusSingleFrame); - - menu.UseKeysUpDownAsKeysLeftRight = true; - Assert.True (menu.UseKeysUpDownAsKeysLeftRight); - Assert.False (menu.UseSubMenusSingleFrame); - - menu.UseSubMenusSingleFrame = true; - Assert.False (menu.UseKeysUpDownAsKeysLeftRight); - Assert.True (menu.UseSubMenusSingleFrame); - } - - [Fact (Skip = "#3798 Broke. Will fix in #2975")] - [AutoInitShutdown] - public void UseSubMenusSingleFrame_False_By_Keyboard () - { - var menu = new MenuBar - { - Menus = new MenuBarItem [] - { - new ( - "Numbers", - new MenuItem [] - { - new ("One", "", null), - new MenuBarItem ( - "Two", - new MenuItem [] - { - new ("Sub-Menu 1", "", null), - new ("Sub-Menu 2", "", null) - } - ), - new ("Three", "", null) - } - ) - } - }; - menu.UseKeysUpDownAsKeysLeftRight = true; + [AutoInitShutdown (configLocation: ConfigLocations.Default)] + public void Disabled_MenuBarItem_Popover_Is_Activated () + { + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); - Assert.False (menu.UseSubMenusSingleFrame); - - top.Draw (); - - var expected = @" - Numbers -"; - - Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - - Assert.True (menu.NewKeyDownEvent (menu.Key)); - top.Draw (); - - expected = @" - Numbers -┌────────┐ -│ One │ -│ Two ►│ -│ Three │ -└────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - - Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorDown)); - top.Draw (); - - expected = @" - Numbers -┌────────┐ -│ One │ -│ Two ►│┌─────────────┐ -│ Three ││ Sub-Menu 1 │ -└────────┘│ Sub-Menu 2 │ - └─────────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - - Assert.True (Application.Top.SubViews.ElementAt (2).NewKeyDownEvent (Key.CursorLeft)); - top.Draw (); - - expected = @" - Numbers -┌────────┐ -│ One │ -│ Two ►│ -│ Three │ -└────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - - Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.Esc)); - top.Draw (); - - expected = @" - Numbers -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - top.Dispose (); - } + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); - [Fact (Skip = "#3798 Broke. Will fix in #2975")] - [AutoInitShutdown] - public void UseSubMenusSingleFrame_False_By_Mouse () - { - var menu = new MenuBar - { - Menus = - [ - new ( - "Numbers", - new MenuItem [] - { - new ("One", "", null), - new MenuBarItem ( - "Two", - new MenuItem [] - { - new ( - "Sub-Menu 1", - "", - null - ), - new ( - "Sub-Menu 2", - "", - null - ) - } - ), - new ("Three", "", null) - } - ) - ] - }; + // Act + menuBarItem.PopoverMenu.Enabled = false; + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.True (menuBar.IsActive ()); + Assert.True (menuBar.IsOpen ()); + Assert.True (menuBarItem.PopoverMenu.Visible); - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); - Assert.False (menu.UseSubMenusSingleFrame); - - top.Draw (); - - var expected = @" - Numbers -"; - - Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 8, 1), pos); - - menu.NewMouseEvent ( - new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } - ); - top.Draw (); - - expected = @" - Numbers -┌────────┐ -│ One │ -│ Two ►│ -│ Three │ -└────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 10, 6), pos); - - menu.NewMouseEvent ( - new () - { - Position = new (1, 2), Flags = MouseFlags.ReportMousePosition, View = Application.Top.SubViews.ElementAt (1) - } - ); - top.Draw (); - - expected = @" - Numbers -┌────────┐ -│ One │ -│ Two ►│┌─────────────┐ -│ Three ││ Sub-Menu 1 │ -└────────┘│ Sub-Menu 2 │ - └─────────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 25, 7), pos); - - Assert.False ( - menu.NewMouseEvent ( - new () - { - Position = new (1, 1), Flags = MouseFlags.ReportMousePosition, View = Application.Top.SubViews.ElementAt (1) - } - ) - ); - top.Draw (); - - expected = @" - Numbers -┌────────┐ -│ One │ -│ Two ►│ -│ Three │ -└────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 10, 6), pos); - - menu.NewMouseEvent ( - new () { Position = new (70, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top } - ); - top.Draw (); - - expected = @" - Numbers -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 8, 1), pos); + Application.End (rs); top.Dispose (); } - [Fact (Skip = "#3798 Broke. Will fix in #2975")] - [AutoInitShutdown] - public void UseSubMenusSingleFrame_False_Disabled_Border () - { - var menu = new MenuBar - { - MenusBorderStyle = LineStyle.None, - Menus = - [ - new ( - "Numbers", - new MenuItem [] - { - new ("One", "", null), - new MenuBarItem ( - "Two", - new MenuItem [] - { - new ( - "Sub-Menu 1", - "", - null - ), - new ( - "Sub-Menu 2", - "", - null - ) - } - ), - new ("Three", "", null) - } - ) - ] - }; - - menu.UseKeysUpDownAsKeysLeftRight = true; - menu.BeginInit (); - menu.EndInit (); - - menu.OpenMenu (); - menu.ColorScheme = menu._openMenu.ColorScheme = new (Attribute.Default); - Assert.True (menu.IsMenuOpen); - - menu.Draw (); - menu._openMenu.Draw (); - - var expected = @" - Numbers - One - Two ► - Three "; - - _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorDown)); - menu.Draw (); - menu._openMenu.Draw (); - menu.OpenCurrentMenu.Draw (); - - expected = @" - Numbers - One - Two ► Sub-Menu 1 - Three Sub-Menu 2"; - - _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - } - - [Fact (Skip = "#3798 Broke. Will fix in #2975")] + [Fact (Skip = "For v2, should the menu close on resize?")] [AutoInitShutdown] - public void UseSubMenusSingleFrame_True_By_Keyboard () + public void Resizing_Closes_Menus () { - var menu = new MenuBar - { - Menus = - [ - new ( - "Numbers", - new MenuItem [] - { - new ("One", "", null), - new MenuBarItem ( - "Two", - new MenuItem [] - { - new ( - "Sub-Menu 1", - "", - null - ), - new ( - "Sub-Menu 2", - "", - null - ) - } - ), - new ("Three", "", null) - } - ) - ] - }; - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); - Assert.False (menu.UseSubMenusSingleFrame); - menu.UseSubMenusSingleFrame = true; - Assert.True (menu.UseSubMenusSingleFrame); - - top.Draw (); - - var expected = @" - Numbers -"; - - Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 8, 1), pos); - - Assert.True (menu.NewKeyDownEvent (menu.Key)); - top.Draw (); - - expected = @" - Numbers -┌────────┐ -│ One │ -│ Two ►│ -│ Three │ -└────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 10, 6), pos); - - Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorDown)); - Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.Enter)); - top.Draw (); - - expected = @" - Numbers -┌─────────────┐ -│◄ Two │ -├─────────────┤ -│ Sub-Menu 1 │ -│ Sub-Menu 2 │ -└─────────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 15, 7), pos); - - Assert.True (Application.Top.SubViews.ElementAt (2).NewKeyDownEvent (Key.Enter)); - top.Draw (); - - expected = @" - Numbers -┌────────┐ -│ One │ -│ Two ►│ -│ Three │ -└────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 10, 6), pos); - - Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.Esc)); - top.Draw (); - - expected = @" - Numbers -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 8, 1), pos); - top.Dispose (); } - [Fact (Skip = "#3798 Broke. Will fix in #2975")] + [Fact] [AutoInitShutdown] - public void UseSubMenusSingleFrame_True_By_Mouse () + public void Update_MenuBarItem_HotKey_Works () { - var menu = new MenuBar - { - Menus = - [ - new ( - "Numbers", - new MenuItem [] - { - new ("One", "", null), - new MenuBarItem ( - "Two", - new MenuItem [] - { - new ( - "Sub-Menu 1", - "", - null - ), - new ( - "Sub-Menu 2", - "", - null - ) - } - ), - new ("Three", "", null) - } - ) - ] - }; - + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); - Assert.False (menu.UseSubMenusSingleFrame); - menu.UseSubMenusSingleFrame = true; - Assert.True (menu.UseSubMenusSingleFrame); - - top.Draw (); - - var expected = @" - Numbers -"; - - Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 8, 1), pos); - - Assert.True (menu.NewMouseEvent (new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu })); - top.Draw (); - - expected = @" - Numbers -┌────────┐ -│ One │ -│ Two ►│ -│ Three │ -└────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 10, 6), pos); - - Assert.False (menu.NewMouseEvent (new () { Position = new (1, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top.SubViews.ElementAt (1) })); - top.Draw (); - - expected = @" - Numbers -┌─────────────┐ -│◄ Two │ -├─────────────┤ -│ Sub-Menu 1 │ -│ Sub-Menu 2 │ -└─────────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 15, 7), pos); - - menu.NewMouseEvent (new () { Position = new (1, 1), Flags = MouseFlags.Button1Clicked, View = Application.Top.SubViews.ElementAt (2) }); - top.Draw (); - - expected = @" - Numbers -┌────────┐ -│ One │ -│ Two ►│ -│ Three │ -└────────┘ -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 10, 6), pos); - - Assert.False (menu.NewMouseEvent (new () { Position = new (70, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top })); - top.Draw (); - - expected = @" - Numbers -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 8, 1), pos); - top.Dispose (); - } - - [Fact (Skip = "#3798 Broke. Will fix in #2975")] - [AutoInitShutdown] - public void UseSubMenusSingleFrame_True_Disabled_Border () - { - var menu = new MenuBar - { - MenusBorderStyle = LineStyle.None, - Menus = - [ - new ( - "Numbers", - new MenuItem [] - { - new ("One", "", null), - new MenuBarItem ( - "Two", - new MenuItem [] - { - new ( - "Sub-Menu 1", - "", - null - ), - new ( - "Sub-Menu 2", - "", - null - ) - } - ), - new ("Three", "", null) - } - ) - ] - }; - - menu.UseSubMenusSingleFrame = true; - menu.BeginInit (); - menu.EndInit (); - - menu.OpenMenu (); - Assert.True (menu.IsMenuOpen); - - menu.Draw (); - menu.ColorScheme = menu._openMenu.ColorScheme = new (Attribute.Default); - menu._openMenu.Draw (); - - var expected = @" - Numbers - One - Two ► - Three "; - - _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - - Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorDown)); - Assert.True (menu._openMenu.NewKeyDownEvent (Key.Enter)); - menu.Draw (); - menu._openMenu.Draw (); - menu.OpenCurrentMenu.Draw (); - - expected = @" - Numbers -◄ Two -───────────── - Sub-Menu 1 - Sub-Menu 2 "; - - _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - } - - [Fact (Skip = "#3798 Broke. Will fix in #2975")] - [AutoInitShutdown] - public void UseSubMenusSingleFrame_True_Without_Border () - { - var menu = new MenuBar - { - UseSubMenusSingleFrame = true, - MenusBorderStyle = LineStyle.None, - Menus = - [ - new ( - "Numbers", - new MenuItem [] - { - new ("One", "", null), - new MenuBarItem ( - "Two", - new MenuItem [] - { - new ( - "Sub-Menu 1", - "", - null - ), - new ( - "Sub-Menu 2", - "", - null - ) - } - ), - new ("Three", "", null) - } - ) - ] - }; + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); + + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.True (menuBar.IsOpen ()); + Assert.True (menuBar.HasFocus); + Assert.True (menuBarItem.PopoverMenu.Visible); + Assert.True (menuBarItem.PopoverMenu.HasFocus); + + // Act + menuBarItem.HotKey = Key.E.WithAlt; + + // old key should do nothing + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.True (menuBar.IsOpen ()); + Assert.True (menuBar.HasFocus); + Assert.True (menuBarItem.PopoverMenu.Visible); + Assert.True (menuBarItem.PopoverMenu.HasFocus); + + // use new key + Application.RaiseKeyDownEvent (Key.E.WithAlt); + Assert.False (menuBar.IsActive ()); + Assert.False (menuBar.IsOpen ()); + Assert.False (menuBar.HasFocus); + Assert.False (menuBar.CanFocus); + Assert.False (menuBarItem.PopoverMenu.Visible); + Assert.False (menuBarItem.PopoverMenu.HasFocus); - var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); - Assert.True (menu.UseSubMenusSingleFrame); - Assert.Equal (LineStyle.None, menu.MenusBorderStyle); - - top.Draw (); - - var expected = @" - Numbers -"; - - Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 8, 1), pos); - - Assert.True ( - menu.NewMouseEvent ( - new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } - ) - ); - top.Draw (); - - expected = @" - Numbers - One - Two ► - Three -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 8, 4), pos); - - menu.NewMouseEvent ( - new () { Position = new (1, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top.SubViews.ElementAt (1) } - ); - top.Draw (); - - expected = @" - Numbers -◄ Two -───────────── - Sub-Menu 1 - Sub-Menu 2 -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 13, 5), pos); - - menu.NewMouseEvent ( - new () { Position = new (1, 1), Flags = MouseFlags.Button1Clicked, View = Application.Top.SubViews.ElementAt (2) } - ); - top.Draw (); - - expected = @" - Numbers - One - Two ► - Three -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 8, 4), pos); - - menu.NewMouseEvent ( - new () { Position = new (70, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top } - ); - top.Draw (); - - expected = @" - Numbers -"; - - pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); - Assert.Equal (new (1, 0, 8, 1), pos); + Application.End (rs); top.Dispose (); } [Fact] [AutoInitShutdown] - public void Visible_False_Key_Does_Not_Open_And_Close_All_Opened_Menus () + public void Visible_False_HotKey_Does_Not_Activate () { - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }) - ] - }; + // Arrange + var menuItem = new MenuItemv2 { Id = "menuItem", Title = "_Item" }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); - - Assert.True (menu.Visible); - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); - - menu.Visible = false; - Assert.False (menu.IsMenuOpen); + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); + + // Act + menuBar.Visible = false; + Application.RaiseKeyDownEvent (Key.N.WithAlt); + Assert.False (menuBar.IsActive ()); + Assert.False (menuBar.IsOpen ()); + Assert.False (menuBar.HasFocus); + Assert.False (menuBar.CanFocus); + Assert.False (menuBarItem.PopoverMenu.Visible); + Assert.False (menuBarItem.PopoverMenu.HasFocus); - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.False (menu.IsMenuOpen); + Application.End (rs); top.Dispose (); } [Fact] [AutoInitShutdown] - public void CanFocus_True_Key_Esc_Exit_Toplevel_If_IsMenuOpen_False () + public void Visible_False_MenuItem_Key_Does_Action () { - var menu = new MenuBar + // Arrange + int action = 0; + var menuItem = new MenuItemv2 () { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }) - ], - CanFocus = true + Id = "menuItem", + Title = "_Item", + Key = Key.F1, + Action = () => action++ }; + var menu = new Menuv2 ([menuItem]) { Id = "menu" }; + var menuBarItem = new MenuBarItemv2 { Id = "menuBarItem", Title = "_New" }; + var menuBarItemPopover = new PopoverMenu (); + menuBarItem.PopoverMenu = menuBarItemPopover; + menuBarItemPopover.Root = menu; + var menuBar = new MenuBarv2 () { Id = "menuBar" }; + menuBar.Add (menuBarItem); + Assert.Single (menuBar.SubViews); + Assert.Single (menuBarItem.SubViews); var top = new Toplevel (); - top.Add (menu); - Application.Begin (top); + top.Add (menuBar); + RunState rs = Application.Begin (top); + Assert.False (menuBar.IsActive ()); - Assert.True (menu.CanFocus); - Assert.True (menu.NewKeyDownEvent (menu.Key)); - Assert.True (menu.IsMenuOpen); + // Act + menuBar.Visible = false; + Application.RaiseKeyDownEvent (Key.F1); - Assert.True (menu.NewKeyDownEvent (Key.Esc)); - Assert.False (menu.IsMenuOpen); + Assert.Equal (1, action); - Assert.False (menu.NewKeyDownEvent (Key.Esc)); - Assert.False (menu.IsMenuOpen); + Application.End (rs); top.Dispose (); } - - // Defines the expected strings for a Menu. Currently supports - // - MenuBar with any number of MenuItems - // - Each top-level MenuItem can have a SINGLE sub-menu - // - // TODO: Enable multiple sub-menus - // TODO: Enable checked sub-menus - // TODO: Enable sub-menus with sub-menus (perhaps better to put this in a separate class with focused unit tests?) - // - // E.g: - // - // File Edit - // New Copy - public class ExpectedMenuBar : MenuBar - { - private FakeDriver _d = (FakeDriver)Application.Driver; - - // The expected strings when the menu is closed - public string ClosedMenuText => MenuBarText + "\n"; - - public string ExpectedBottomRow (int i) - { - return $"{Glyphs.LLCorner}{new (Glyphs.HLine.ToString () [0], Menus [i].Children [0].TitleLength + 3)}{Glyphs.LRCorner} \n"; - } - - // The 3 spaces at end are a result of Menu.cs line 1062 where `pos` is calculated (` + spacesAfterTitle`) - public string ExpectedMenuItemRow (int i) { return $"{Glyphs.VLine} {Menus [i].Children [0].Title} {Glyphs.VLine} \n"; } - - // The full expected string for an open sub menu - public string ExpectedSubMenuOpen (int i) - { - return ClosedMenuText - + (Menus [i].Children.Length > 0 - ? ExpectedPadding (i) - + ExpectedTopRow (i) - + ExpectedPadding (i) - + ExpectedMenuItemRow (i) - + ExpectedPadding (i) - + ExpectedBottomRow (i) - : ""); - } - - // Define expected menu frame - // "┌──────┐" - // "│ New │" - // "└──────┘" - // - // The width of the Frame is determined in Menu.cs line 144, where `Width` is calculated - // 1 space before the Title and 2 spaces after the Title/Check/Help - public string ExpectedTopRow (int i) - { - return $"{Glyphs.ULCorner}{new (Glyphs.HLine.ToString () [0], Menus [i].Children [0].TitleLength + 3)}{Glyphs.URCorner} \n"; - } - - // Each MenuBar title has a 1 space pad on each side - // See `static int leftPadding` and `static int rightPadding` on line 1037 of Menu.cs - public string MenuBarText - { - get - { - var txt = string.Empty; - - foreach (MenuBarItem m in Menus) - { - txt += " " + m.Title + " "; - } - - return txt; - } - } - - // Padding for the X of the sub menu Frame - // Menu.cs - Line 1239 in `internal void OpenMenu` is where the Menu is created - private string ExpectedPadding (int i) - { - var n = 0; - - while (i > 0) - { - n += Menus [i - 1].TitleLength + 2; - i--; - } - - return new (' ', n); - } - } - - private class CustomWindow : Window - { - public CustomWindow () - { - var menu = new MenuBar - { - Menus = - [ - new ("File", new MenuItem [] { new ("New", "", null) }), - new ( - "Edit", - new MenuItem [] - { - new MenuBarItem ( - "Delete", - new MenuItem [] - { new ("All", "", null), new ("Selected", "", null) } - ) - } - ) - ] - }; - Add (menu); - } - } } diff --git a/Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs b/Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs new file mode 100644 index 0000000000..e050b816e1 --- /dev/null +++ b/Tests/UnitTests/Views/Menuv1/MenuBarv1Tests.cs @@ -0,0 +1,3886 @@ +using UnitTests; +using Xunit.Abstractions; + +namespace Terminal.Gui.ViewsTests; + +public class MenuBarv1Tests (ITestOutputHelper output) +{ + [Fact] + [AutoInitShutdown] + public void AddMenuBarItem_RemoveMenuItem_Dynamically () + { + var menuBar = new MenuBar (); + var menuBarItem = new MenuBarItem { Title = "_New" }; + var action = ""; + var menuItem = new MenuItem { Title = "_Item", Action = () => action = "I", Parent = menuBarItem }; + Assert.Equal ("n", menuBarItem.HotKey); + Assert.Equal ("i", menuItem.HotKey); + Assert.Empty (menuBar.Menus); + menuBarItem.AddMenuBarItem (menuBar, menuItem); + menuBar.Menus = [menuBarItem]; + Assert.Single (menuBar.Menus); + Assert.Single (menuBar.Menus [0].Children!); + + Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); + Assert.False (menuBar.HotKeyBindings.TryGet (Key.I, out _)); + + var top = new Toplevel (); + top.Add (menuBar); + Application.Begin (top); + + top.NewKeyDownEvent (Key.N.WithAlt); + Application.MainLoop.RunIteration (); + Assert.True (menuBar.IsMenuOpen); + Assert.Equal ("", action); + + top.NewKeyDownEvent (Key.I); + Application.MainLoop.RunIteration (); + Assert.False (menuBar.IsMenuOpen); + Assert.Equal ("I", action); + + menuItem.RemoveMenuItem (); + Assert.Single (menuBar.Menus); + Assert.Null (menuBar.Menus [0].Children); + Assert.True (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); + Assert.False (menuBar.HotKeyBindings.TryGet (Key.I, out _)); + + menuBarItem.RemoveMenuItem (); + Assert.Empty (menuBar.Menus); + Assert.False (menuBar.HotKeyBindings.TryGet (Key.N.WithAlt, out _)); + + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void AllowNullChecked_Get_Set () + { + var mi = new MenuItem ("Check this out 你", "", null) { CheckType = MenuItemCheckStyle.Checked }; + mi.Action = mi.ToggleChecked; + + var menu = new MenuBar + { + Menus = + [ + new ("Nullable Checked", new [] { mi }) + ] + }; + + //new CheckBox (); + Toplevel top = new (); + top.Add (menu); + Application.Begin (top); + + Assert.False (mi.Checked); + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.True (menu._openMenu.NewKeyDownEvent (Key.Enter)); + Application.MainLoop.RunIteration (); + Assert.True (mi.Checked); + + Assert.True ( + menu.NewMouseEvent ( + new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } + ) + ); + + Assert.True ( + menu._openMenu.NewMouseEvent ( + new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked, View = menu._openMenu } + ) + ); + Application.MainLoop.RunIteration (); + Assert.False (mi.Checked); + + mi.AllowNullChecked = true; + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.True (menu._openMenu.NewKeyDownEvent (Key.Enter)); + Application.MainLoop.RunIteration (); + Assert.Null (mi.Checked); + + Assert.True ( + menu.NewMouseEvent ( + new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } + ) + ); + Application.LayoutAndDraw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @$" + Nullable Checked +┌──────────────────────┐ +│ {Glyphs.CheckStateNone} Check this out 你 │ +└──────────────────────┘", + output + ); + + Assert.True ( + menu._openMenu.NewMouseEvent ( + new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked, View = menu._openMenu } + ) + ); + Application.MainLoop.RunIteration (); + Assert.True (mi.Checked); + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.True (menu._openMenu.NewKeyDownEvent (Key.Enter)); + Application.MainLoop.RunIteration (); + Assert.False (mi.Checked); + + Assert.True ( + menu.NewMouseEvent ( + new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } + ) + ); + + Assert.True ( + menu._openMenu.NewMouseEvent ( + new () { Position = new (0, 0), Flags = MouseFlags.Button1Clicked, View = menu._openMenu } + ) + ); + Application.MainLoop.RunIteration (); + Assert.Null (mi.Checked); + + mi.AllowNullChecked = false; + Assert.False (mi.Checked); + + mi.CheckType = MenuItemCheckStyle.NoCheck; + Assert.Throws (mi.ToggleChecked); + + mi.CheckType = MenuItemCheckStyle.Radio; + Assert.Throws (mi.ToggleChecked); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void CanExecute_False_Does_Not_Throws () + { + var menu = new MenuBar + { + Menus = + [ + new ("File", new MenuItem [] + { + new ("New", "", null, () => false), + null, + new ("Quit", "", null) + }) + ] + }; + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.True (menu.IsMenuOpen); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void CanExecute_HotKey () + { + Window win = null; + + var menu = new MenuBar + { + Menus = + [ + new ( + "_File", + new MenuItem [] + { + new ("_New", "", New, CanExecuteNew), + new ( + "_Close", + "", + Close, + CanExecuteClose + ) + } + ) + ] + }; + Toplevel top = new (); + top.Add (menu); + + bool CanExecuteNew () { return win == null; } + + void New () { win = new (); } + + bool CanExecuteClose () { return win != null; } + + void Close () { win = null; } + + Application.Begin (top); + + Assert.Null (win); + Assert.True (CanExecuteNew ()); + Assert.False (CanExecuteClose ()); + + Assert.True (top.NewKeyDownEvent (Key.F.WithAlt)); + Assert.True (top.NewKeyDownEvent (Key.N.WithAlt)); + Application.MainLoop.RunIteration (); + Assert.NotNull (win); + Assert.False (CanExecuteNew ()); + Assert.True (CanExecuteClose ()); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void Click_Another_View_Close_An_Open_Menu () + { + var menu = new MenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("New", "", null) }) + ] + }; + + var btnClicked = false; + var btn = new Button { Y = 4, Text = "Test" }; + btn.Accepting += (s, e) => btnClicked = true; + var top = new Toplevel (); + top.Add (menu, btn); + Application.Begin (top); + + Application.RaiseMouseEvent (new () { ScreenPosition = new (0, 4), Flags = MouseFlags.Button1Clicked }); + Assert.True (btnClicked); + top.Dispose (); + } + + // TODO: Lots of tests in here really test Menu and MenuItem - Move them to MenuTests.cs + + [Fact] + public void Constructors_Defaults () + { + var menuBar = new MenuBar (); + Assert.Equal (KeyCode.F9, menuBar.Key); + var menu = new Menu { Host = menuBar, X = 0, Y = 0, BarItems = new () }; + Assert.Null (menu.ColorScheme); + Assert.False (menu.IsInitialized); + menu.BeginInit (); + menu.EndInit (); + Assert.Equal (Colors.ColorSchemes ["Menu"], menu.ColorScheme); + Assert.True (menu.CanFocus); + Assert.False (menu.WantContinuousButtonPressed); + Assert.Equal (LineStyle.Single, menuBar.MenusBorderStyle); + + menuBar = new (); + Assert.Equal (0, menuBar.X); + Assert.Equal (0, menuBar.Y); + Assert.IsType (menuBar.Width); + Assert.Equal (1, menuBar.Height); + Assert.Empty (menuBar.Menus); + Assert.Equal (Colors.ColorSchemes ["Menu"], menuBar.ColorScheme); + Assert.True (menuBar.WantMousePositionReports); + Assert.False (menuBar.IsMenuOpen); + + menuBar = new () { Menus = [] }; + Assert.Equal (0, menuBar.X); + Assert.Equal (0, menuBar.Y); + Assert.IsType (menuBar.Width); + Assert.Equal (1, menuBar.Height); + Assert.Empty (menuBar.Menus); + Assert.Equal (Colors.ColorSchemes ["Menu"], menuBar.ColorScheme); + Assert.True (menuBar.WantMousePositionReports); + Assert.False (menuBar.IsMenuOpen); + + var menuBarItem = new MenuBarItem (); + Assert.Equal ("", menuBarItem.Title); + Assert.Null (menuBarItem.Parent); + Assert.Empty (menuBarItem.Children); + + menuBarItem = new (new MenuBarItem [] { }); + Assert.Equal ("", menuBarItem.Title); + Assert.Null (menuBarItem.Parent); + Assert.Empty (menuBarItem.Children); + + menuBarItem = new ("Test", new MenuBarItem [] { }); + Assert.Equal ("Test", menuBarItem.Title); + Assert.Null (menuBarItem.Parent); + Assert.Empty (menuBarItem.Children); + + menuBarItem = new ("Test", new List ()); + Assert.Equal ("Test", menuBarItem.Title); + Assert.Null (menuBarItem.Parent); + Assert.Empty (menuBarItem.Children); + + menuBarItem = new ("Test", "Help", null); + Assert.Equal ("Test", menuBarItem.Title); + Assert.Equal ("Help", menuBarItem.Help); + Assert.Null (menuBarItem.Action); + Assert.Null (menuBarItem.CanExecute); + Assert.Null (menuBarItem.Parent); + Assert.Equal (Key.Empty, menuBarItem.ShortcutKey); + } + + [Fact] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] + public void Disabled_MenuBar_Is_Never_Opened () + { + Toplevel top = new (); + + var menu = new MenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("New", "", null) }) + ] + }; + top.Add (menu); + Application.Begin (top); + Assert.True (menu.Enabled); + menu.OpenMenu (); + Assert.True (menu.IsMenuOpen); + + menu.Enabled = false; + menu.CloseAllMenus (); + menu.OpenMenu (); + Assert.False (menu.IsMenuOpen); + top.Dispose (); + } + + [Fact (Skip = "#3798 Broke. Will fix in #2975")] + [AutoInitShutdown (configLocation: ConfigLocations.Default)] + public void Disabled_MenuItem_Is_Never_Selected () + { + var menu = new MenuBar + { + Menus = + [ + new ( + "Menu", + new MenuItem [] + { + new ("Enabled 1", "", null), + new ("Disabled", "", null, () => false), + null, + new ("Enabled 2", "", null) + } + ) + ] + }; + + Toplevel top = new (); + top.Add (menu); + Application.Begin (top); + + Attribute [] attributes = + { + // 0 + menu.ColorScheme.Normal, + + // 1 + menu.ColorScheme.Focus, + + // 2 + menu.ColorScheme.Disabled + }; + + DriverAssert.AssertDriverAttributesAre ( + @" +00000000000000", + output, + Application.Driver, + attributes + ); + + Assert.True ( + menu.NewMouseEvent ( + new () { Position = new (0, 0), Flags = MouseFlags.Button1Pressed, View = menu } + ) + ); + top.Draw (); + + DriverAssert.AssertDriverAttributesAre ( + @" +11111100000000 +00000000000000 +01111111111110 +02222222222220 +00000000000000 +00000000000000 +00000000000000", + output, + Application.Driver, + attributes + ); + + Assert.True ( + top.SubViews.ElementAt (1) + .NewMouseEvent ( + new () { Position = new (0, 2), Flags = MouseFlags.Button1Clicked, View = top.SubViews.ElementAt (1) } + ) + ); + top.SubViews.ElementAt (1).Layout(); + top.SubViews.ElementAt (1).Draw (); + + DriverAssert.AssertDriverAttributesAre ( + @" +11111100000000 +00000000000000 +01111111111110 +02222222222220 +00000000000000 +00000000000000 +00000000000000", + output, + Application.Driver, + attributes + ); + + Assert.True ( + top.SubViews.ElementAt (1) + .NewMouseEvent ( + new () { Position = new (0, 2), Flags = MouseFlags.ReportMousePosition, View = top.SubViews.ElementAt (1) } + ) + ); + top.SubViews.ElementAt (1).Draw (); + + DriverAssert.AssertDriverAttributesAre ( + @" +11111100000000 +00000000000000 +01111111111110 +02222222222220 +00000000000000 +00000000000000 +00000000000000", + output, + Application.Driver, + attributes + ); + top.Dispose (); + } + + [Fact (Skip = "#3798 Broke. Will fix in #2975")] + [AutoInitShutdown] + public void Draw_A_Menu_Over_A_Dialog () + { + // Override CM + Window.DefaultBorderStyle = LineStyle.Single; + Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; + Dialog.DefaultShadow = ShadowStyle.None; + Button.DefaultShadow = ShadowStyle.None; + + Toplevel top = new (); + var win = new Window (); + top.Add (win); + RunState rsTop = Application.Begin (top); + ((FakeDriver)Application.Driver!).SetBufferSize (40, 15); + + Assert.Equal (new (0, 0, 40, 15), win.Frame); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + List items = new () + { + "New", + "Open", + "Close", + "Save", + "Save As", + "Delete" + }; + var dialog = new Dialog { X = 2, Y = 2, Width = 15, Height = 4 }; + var menu = new MenuBar { X = Pos.Center (), Width = 10 }; + + menu.Menus = new MenuBarItem [] + { + new ( + "File", + new MenuItem [] + { + new ( + items [0], + "Create a new file", + () => ChangeMenuTitle ("New"), + null, + null, + KeyCode.CtrlMask | KeyCode.N + ), + new ( + items [1], + "Open a file", + () => ChangeMenuTitle ("Open"), + null, + null, + KeyCode.CtrlMask | KeyCode.O + ), + new ( + items [2], + "Close a file", + () => ChangeMenuTitle ("Close"), + null, + null, + KeyCode.CtrlMask | KeyCode.C + ), + new ( + items [3], + "Save a file", + () => ChangeMenuTitle ("Save"), + null, + null, + KeyCode.CtrlMask | KeyCode.S + ), + new ( + items [4], + "Save a file as", + () => ChangeMenuTitle ("Save As"), + null, + null, + KeyCode.CtrlMask | KeyCode.A + ), + new ( + items [5], + "Delete a file", + () => ChangeMenuTitle ("Delete"), + null, + null, + KeyCode.CtrlMask | KeyCode.A + ) + } + ) + }; + dialog.Add (menu); + + void ChangeMenuTitle (string title) + { + menu.Menus [0].Title = title; + menu.SetNeedsDraw (); + } + + RunState rsDialog = Application.Begin (dialog); + Application.RunIteration (ref rsDialog); + + Assert.Equal (new (2, 2, 15, 4), dialog.Frame); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ │ +│ ┌─────────────┐ │ +│ │ File │ │ +│ │ │ │ +│ └─────────────┘ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Assert.Equal ("File", menu.Menus [0].Title); + menu.OpenMenu (); + Application.RunIteration (ref rsDialog); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ │ +│ ┌─────────────┐ │ +│ │ File │ │ +│ │ ┌──────────────────────────────────┐ +│ └─│ New Create a new file Ctrl+N │ +│ │ Open Open a file Ctrl+O │ +│ │ Close Close a file Ctrl+C │ +│ │ Save Save a file Ctrl+S │ +│ │ Save As Save a file as Ctrl+A │ +│ │ Delete Delete a file Ctrl+A │ +│ └──────────────────────────────────┘ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Application.RaiseMouseEvent (new () { ScreenPosition = new (20, 5), Flags = MouseFlags.Button1Clicked }); + + // Need to fool MainLoop into thinking it's running + Application.MainLoop.Running = true; + bool firstIteration = true; + Application.RunIteration (ref rsDialog, firstIteration); + Assert.Equal (items [0], menu.Menus [0].Title); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ │ +│ ┌─────────────┐ │ +│ │ New │ │ +│ │ │ │ +│ └─────────────┘ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + for (var i = 0; i < items.Count; i++) + { + menu.OpenMenu (); + + Application.RaiseMouseEvent (new () { ScreenPosition = new (20, 5 + i), Flags = MouseFlags.Button1Clicked }); + + Application.RunIteration (ref rsDialog); + Assert.Equal (items [i], menu.Menus [0].Title); + } + + ((FakeDriver)Application.Driver!).SetBufferSize (20, 15); + menu.OpenMenu (); + Application.RunIteration (ref rsDialog); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────┐ +│ │ +│ ┌─────────────┐ │ +│ │ Delete │ │ +│ │ ┌─────────────── +│ └─│ New Create +│ │ Open O +│ │ Close Cl +│ │ Save S +│ │ Save As Save +│ │ Delete Del +│ └─────────────── +│ │ +│ │ +└──────────────────┘", + output + ); + + Application.End (rsDialog); + Application.End (rsTop); + top.Dispose (); + } + + [Fact (Skip = "#3798 Broke. Will fix in #2975")] + [AutoInitShutdown] + public void Draw_A_Menu_Over_A_Top_Dialog () + { + ((FakeDriver)Application.Driver).SetBufferSize (40, 15); + + // Override CM + Window.DefaultBorderStyle = LineStyle.Single; + Dialog.DefaultButtonAlignment = Alignment.Center; + Dialog.DefaultBorderStyle = LineStyle.Single; + Dialog.DefaultShadow = ShadowStyle.None; + Button.DefaultShadow = ShadowStyle.None; + + Assert.Equal (new (0, 0, 40, 15), View.GetClip ()!.GetBounds()); + DriverAssert.AssertDriverContentsWithFrameAre (@"", output); + + List items = new () + { + "New", + "Open", + "Close", + "Save", + "Save As", + "Delete" + }; + var dialog = new Dialog { X = 2, Y = 2, Width = 15, Height = 4 }; + var menu = new MenuBar { X = Pos.Center (), Width = 10 }; + + menu.Menus = new MenuBarItem [] + { + new ( + "File", + new MenuItem [] + { + new ( + items [0], + "Create a new file", + () => ChangeMenuTitle ("New"), + null, + null, + KeyCode.CtrlMask | KeyCode.N + ), + new ( + items [1], + "Open a file", + () => ChangeMenuTitle ("Open"), + null, + null, + KeyCode.CtrlMask | KeyCode.O + ), + new ( + items [2], + "Close a file", + () => ChangeMenuTitle ("Close"), + null, + null, + KeyCode.CtrlMask | KeyCode.C + ), + new ( + items [3], + "Save a file", + () => ChangeMenuTitle ("Save"), + null, + null, + KeyCode.CtrlMask | KeyCode.S + ), + new ( + items [4], + "Save a file as", + () => ChangeMenuTitle ("Save As"), + null, + null, + KeyCode.CtrlMask | KeyCode.A + ), + new ( + items [5], + "Delete a file", + () => ChangeMenuTitle ("Delete"), + null, + null, + KeyCode.CtrlMask | KeyCode.A + ) + } + ) + }; + dialog.Add (menu); + + void ChangeMenuTitle (string title) + { + menu.Menus [0].Title = title; + menu.SetNeedsDraw (); + } + + RunState rs = Application.Begin (dialog); + Application.RunIteration (ref rs); + + Assert.Equal (new (2, 2, 15, 4), dialog.Frame); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" + ┌─────────────┐ + │ File │ + │ │ + └─────────────┘", + output + ); + + Assert.Equal ("File", menu.Menus [0].Title); + menu.OpenMenu (); + Application.RunIteration (ref rs); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" + ┌─────────────┐ + │ File │ + │ ┌──────────────────────────────────┐ + └─│ New Create a new file Ctrl+N │ + │ Open Open a file Ctrl+O │ + │ Close Close a file Ctrl+C │ + │ Save Save a file Ctrl+S │ + │ Save As Save a file as Ctrl+A │ + │ Delete Delete a file Ctrl+A │ + └──────────────────────────────────┘", + output + ); + + Application.RaiseMouseEvent (new () { ScreenPosition = new (20, 5), Flags = MouseFlags.Button1Clicked }); + + // Need to fool MainLoop into thinking it's running + Application.MainLoop.Running = true; + Application.RunIteration (ref rs); + Assert.Equal (items [0], menu.Menus [0].Title); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" + ┌─────────────┐ + │ New │ + │ │ + └─────────────┘", + output + ); + + for (var i = 1; i < items.Count; i++) + { + menu.OpenMenu (); + + Application.RaiseMouseEvent (new () { ScreenPosition = new (20, 5 + i), Flags = MouseFlags.Button1Clicked }); + + Application.RunIteration (ref rs); + Assert.Equal (items [i], menu.Menus [0].Title); + } + + ((FakeDriver)Application.Driver!).SetBufferSize (20, 15); + menu.OpenMenu (); + Application.RunIteration (ref rs); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" + ┌─────────────┐ + │ Delete │ + │ ┌─────────────── + └─│ New Create + │ Open O + │ Close Cl + │ Save S + │ Save As Save + │ Delete Del + └───────────────", + output + ); + + Application.End (rs); + dialog.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void DrawFrame_With_Negative_Positions () + { + var menu = new MenuBar + { + X = -1, + Y = -1, + Menus = + [ + new (new MenuItem [] { new ("One", "", null), new ("Two", "", null) }) + ] + }; + menu.Layout (); + + Assert.Equal (new (-1, -1), new Point (menu.Frame.X, menu.Frame.Y)); + + Toplevel top = new (); + Application.Begin (top); + menu.OpenMenu (); + Application.LayoutAndDraw (); + + var expected = @" +──────┐ + One │ + Two │ +──────┘ +"; + + Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (0, 0, 7, 4), pos); + + menu.CloseAllMenus (); + menu.Frame = new (-1, -2, menu.Frame.Width, menu.Frame.Height); + menu.OpenMenu (); + Application.LayoutAndDraw (); + + expected = @" + One │ + Two │ +──────┘ +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 7, 3), pos); + + menu.CloseAllMenus (); + menu.Frame = new (0, 0, menu.Frame.Width, menu.Frame.Height); + ((FakeDriver)Application.Driver!).SetBufferSize (7, 5); + menu.OpenMenu (); + Application.LayoutAndDraw (); + + expected = @" +┌────── +│ One +│ Two +└────── +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (0, 1, 7, 4), pos); + + menu.CloseAllMenus (); + menu.Frame = new (0, 0, menu.Frame.Width, menu.Frame.Height); + ((FakeDriver)Application.Driver!).SetBufferSize (7, 3); + menu.OpenMenu (); + Application.LayoutAndDraw (); + + expected = @" +┌────── +│ One +│ Two +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (0, 0, 7, 3), pos); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void DrawFrame_With_Negative_Positions_Disabled_Border () + { + var menu = new MenuBar + { + X = -2, + Y = -1, + MenusBorderStyle = LineStyle.None, + Menus = + [ + new (new MenuItem [] { new ("One", "", null), new ("Two", "", null) }) + ] + }; + menu.Layout (); + + Assert.Equal (new (-2, -1), new Point (menu.Frame.X, menu.Frame.Y)); + + Toplevel top = new (); + Application.Begin (top); + menu.OpenMenu (); + Application.LayoutAndDraw (); + + var expected = @" +ne +wo +"; + + _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + + menu.CloseAllMenus (); + menu.Frame = new (-2, -2, menu.Frame.Width, menu.Frame.Height); + menu.OpenMenu (); + Application.LayoutAndDraw (); + + expected = @" +wo +"; + + _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + + menu.CloseAllMenus (); + menu.Frame = new (0, 0, menu.Frame.Width, menu.Frame.Height); + ((FakeDriver)Application.Driver!).SetBufferSize (3, 2); + menu.OpenMenu (); + Application.LayoutAndDraw (); + + expected = @" + On + Tw +"; + + _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + + menu.CloseAllMenus (); + menu.Frame = new (0, 0, menu.Frame.Width, menu.Frame.Height); + ((FakeDriver)Application.Driver!).SetBufferSize (3, 1); + menu.OpenMenu (); + Application.LayoutAndDraw (); + + expected = @" + On +"; + + _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void DrawFrame_With_Positive_Positions () + { + var menu = new MenuBar + { + Menus = + [ + new (new MenuItem [] { new ("One", "", null), new ("Two", "", null) }) + ] + }; + + Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); + + Toplevel top = new (); + Application.Begin (top); + menu.OpenMenu (); + Application.LayoutAndDraw (); + + var expected = @" +┌──────┐ +│ One │ +│ Two │ +└──────┘ +"; + + Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (0, 1, 8, 4), pos); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void DrawFrame_With_Positive_Positions_Disabled_Border () + { + var menu = new MenuBar + { + MenusBorderStyle = LineStyle.None, + Menus = + [ + new (new MenuItem [] { new ("One", "", null), new ("Two", "", null) }) + ] + }; + + Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); + + Toplevel top = new (); + Application.Begin (top); + menu.OpenMenu (); + Application.LayoutAndDraw (); + + var expected = @" + One + Two +"; + + _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + top.Dispose (); + } + + [Fact] + public void Exceptions () + { + Assert.Throws (() => new MenuBarItem ("Test", (MenuItem [])null)); + Assert.Throws (() => new MenuBarItem ("Test", (List)null)); + } + + [Fact] + [AutoInitShutdown] + public void HotKey_MenuBar_OnKeyDown_OnKeyUp_ProcessKeyPressed () + { + var newAction = false; + var copyAction = false; + + var menu = new MenuBar + { + Menus = + [ + new ("_File", new MenuItem [] { new ("_New", "", () => newAction = true) }), + new ( + "_Edit", + new MenuItem [] { new ("_Copy", "", () => copyAction = true) } + ) + ] + }; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Assert.False (newAction); + Assert.False (copyAction); + +#if SUPPORT_ALT_TO_ACTIVATE_MENU + Assert.False (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); + Assert.False (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); + Assert.True (Application.Top.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); + Assert.True (menu.IsMenuOpen); + Application.Top.Draw (); + + string expected = @" + File Edit +"; + + var pos = DriverAsserts.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 11, 1), pos); + + Assert.True (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.N))); + Application.MainLoop.RunIteration (); + Assert.False (newAction); // not yet, hot keys don't work if the item is not visible + + Assert.True (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.F))); + Application.MainLoop.RunIteration (); + Assert.True (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.N))); + Application.MainLoop.RunIteration (); + Assert.True (newAction); + Application.Top.Draw (); + + expected = @" + File Edit +"; + + Assert.False (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.AltMask))); + Assert.True (Application.Top.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); + Assert.True (Application.Top.ProcessKeyUp (new KeyEventArgs (Key.AltMask))); + Assert.True (menu.IsMenuOpen); + Application.Top.Draw (); + + expected = @" + File Edit +"; + + pos = DriverAsserts.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 11, 1), pos); + + Assert.True (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.CursorRight))); + Assert.True (Application.Top.ProcessKeyDown (new KeyEventArgs (Key.C))); + Application.MainLoop.RunIteration (); + Assert.True (copyAction); +#endif + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void HotKey_MenuBar_ProcessKeyPressed_Menu_ProcessKey () + { + var newAction = false; + var copyAction = false; + + // Define the expected menu + var expectedMenu = new ExpectedMenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("New", "", null) }), + new ( + "Edit", + new MenuItem [] { new ("Copy", "", null) } + ) + ] + }; + + // The real menu + var menu = new MenuBar + { + Menus = + [ + new ( + "_" + expectedMenu.Menus [0].Title, + new MenuItem [] + { + new ( + "_" + expectedMenu.Menus [0].Children [0].Title, + "", + () => newAction = true + ) + } + ), + new ( + "_" + expectedMenu.Menus [1].Title, + new MenuItem [] + { + new ( + "_" + + expectedMenu.Menus [1] + .Children [0] + .Title, + "", + () => copyAction = true + ) + } + ) + ] + }; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Assert.False (newAction); + Assert.False (copyAction); + + Assert.True (menu.NewKeyDownEvent (Key.F.WithAlt)); + Assert.True (menu.IsMenuOpen); + Application.Top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); + + Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.N)); + Application.MainLoop.RunIteration (); + Assert.True (newAction); + + Assert.True (menu.NewKeyDownEvent (Key.E.WithAlt)); + Assert.True (menu.IsMenuOpen); + Application.Top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); + + Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.C)); + Application.MainLoop.RunIteration (); + Assert.True (copyAction); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void Key_Open_And_Close_The_MenuBar () + { + var menu = new MenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("New", "", null) }) + ] + }; + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Assert.True (top.NewKeyDownEvent (menu.Key)); + Assert.True (menu.IsMenuOpen); + Assert.True (top.NewKeyDownEvent (menu.Key)); + Assert.False (menu.IsMenuOpen); + + menu.Key = Key.F10.WithShift; + Assert.False (top.NewKeyDownEvent (Key.F9)); + Assert.False (menu.IsMenuOpen); + + Assert.True (top.NewKeyDownEvent (Key.F10.WithShift)); + Assert.True (menu.IsMenuOpen); + Assert.True (top.NewKeyDownEvent (Key.F10.WithShift)); + Assert.False (menu.IsMenuOpen); + top.Dispose (); + } + + [Theory] + [AutoInitShutdown] + [InlineData ("_File", "_New", "", KeyCode.Space | KeyCode.CtrlMask)] + [InlineData ("Closed", "None", "", KeyCode.Space | KeyCode.CtrlMask, KeyCode.Space | KeyCode.CtrlMask)] + [InlineData ("_File", "_New", "", KeyCode.F9)] + [InlineData ("Closed", "None", "", KeyCode.F9, KeyCode.F9)] + [InlineData ("_File", "_Open", "", KeyCode.F9, KeyCode.CursorDown)] + [InlineData ("_File", "_Save", "", KeyCode.F9, KeyCode.CursorDown, KeyCode.CursorDown)] + [InlineData ("_File", "_Quit", "", KeyCode.F9, KeyCode.CursorDown, KeyCode.CursorDown, KeyCode.CursorDown)] + [InlineData ( + "_File", + "_New", + "", + KeyCode.F9, + KeyCode.CursorDown, + KeyCode.CursorDown, + KeyCode.CursorDown, + KeyCode.CursorDown + )] + [InlineData ("_File", "_New", "", KeyCode.F9, KeyCode.CursorDown, KeyCode.CursorUp)] + [InlineData ("_File", "_Quit", "", KeyCode.F9, KeyCode.CursorUp)] + [InlineData ("_File", "_New", "", KeyCode.F9, KeyCode.CursorUp, KeyCode.CursorDown)] + [InlineData ("Closed", "None", "Open", KeyCode.F9, KeyCode.CursorDown, KeyCode.Enter)] + [InlineData ("_Edit", "_Copy", "", KeyCode.F9, KeyCode.CursorRight)] + [InlineData ("_About", "_About", "", KeyCode.F9, KeyCode.CursorLeft)] + [InlineData ("_Edit", "_Copy", "", KeyCode.F9, KeyCode.CursorLeft, KeyCode.CursorLeft)] + [InlineData ("_Edit", "_Select All", "", KeyCode.F9, KeyCode.CursorRight, KeyCode.CursorUp)] + [InlineData ("_File", "_New", "", KeyCode.F9, KeyCode.CursorRight, KeyCode.CursorDown, KeyCode.CursorLeft)] + [InlineData ("_About", "_About", "", KeyCode.F9, KeyCode.CursorRight, KeyCode.CursorRight)] + [InlineData ("Closed", "None", "New", KeyCode.F9, KeyCode.Enter)] + [InlineData ("Closed", "None", "Quit", KeyCode.F9, KeyCode.CursorUp, KeyCode.Enter)] + [InlineData ("Closed", "None", "Copy", KeyCode.F9, KeyCode.CursorRight, KeyCode.Enter)] + [InlineData ( + "Closed", + "None", + "Find", + KeyCode.F9, + KeyCode.CursorRight, + KeyCode.CursorUp, + KeyCode.CursorUp, + KeyCode.Enter + )] + [InlineData ( + "Closed", + "None", + "Replace", + KeyCode.F9, + KeyCode.CursorRight, + KeyCode.CursorUp, + KeyCode.CursorUp, + KeyCode.CursorDown, + KeyCode.Enter + )] + [InlineData ( + "_Edit", + "F_ind", + "", + KeyCode.F9, + KeyCode.CursorRight, + KeyCode.CursorUp, + KeyCode.CursorUp, + KeyCode.CursorLeft, + KeyCode.Enter + )] + [InlineData ("Closed", "None", "About", KeyCode.F9, KeyCode.CursorRight, KeyCode.CursorRight, KeyCode.Enter)] + + //// Hotkeys + [InlineData ("_File", "_New", "", KeyCode.AltMask | KeyCode.F)] + [InlineData ("Closed", "None", "", KeyCode.AltMask | KeyCode.ShiftMask | KeyCode.F)] + [InlineData ("Closed", "None", "", KeyCode.AltMask | KeyCode.F, KeyCode.Esc)] + [InlineData ("Closed", "None", "", KeyCode.AltMask | KeyCode.F, KeyCode.AltMask | KeyCode.F)] + [InlineData ("Closed", "None", "Open", KeyCode.AltMask | KeyCode.F, KeyCode.O)] + [InlineData ("_File", "_New", "", KeyCode.AltMask | KeyCode.F, KeyCode.ShiftMask | KeyCode.O)] + [InlineData ("Closed", "None", "Open", KeyCode.AltMask | KeyCode.F, KeyCode.AltMask | KeyCode.O)] + [InlineData ("_Edit", "_Copy", "", KeyCode.AltMask | KeyCode.E)] + [InlineData ("_Edit", "F_ind", "", KeyCode.AltMask | KeyCode.E, KeyCode.F)] + [InlineData ("_Edit", "F_ind", "", KeyCode.AltMask | KeyCode.E, KeyCode.AltMask | KeyCode.F)] + [InlineData ("Closed", "None", "Replace", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.R)] + [InlineData ("Closed", "None", "Copy", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.C)] + [InlineData ("_Edit", "_1st", "", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3)] + [InlineData ("Closed", "None", "1", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3, KeyCode.D1)] + [InlineData ("Closed", "None", "1", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3, KeyCode.Enter)] + [InlineData ("Closed", "None", "2", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3, KeyCode.D2)] + [InlineData ("_Edit", "_5th", "", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D3, KeyCode.D4)] + [InlineData ("Closed", "None", "5", KeyCode.AltMask | KeyCode.E, KeyCode.F, KeyCode.D4, KeyCode.D5)] + [InlineData ("Closed", "None", "About", KeyCode.AltMask | KeyCode.A)] + public void KeyBindings_Navigation_Commands ( + string expectedBarTitle, + string expectedItemTitle, + string expectedAction, + params KeyCode [] keys + ) + { + var miAction = ""; + MenuItem mbiCurrent = null; + MenuItem miCurrent = null; + + var menu = new MenuBar (); + + Func fn = s => + { + miAction = s as string; + + return true; + }; + menu.EnableForDesign (ref fn); + + menu.Key = KeyCode.F9; + menu.MenuOpening += (s, e) => mbiCurrent = e.CurrentMenu; + menu.MenuOpened += (s, e) => { miCurrent = e.MenuItem; }; + + menu.MenuClosing += (s, e) => + { + mbiCurrent = null; + miCurrent = null; + }; + menu.UseKeysUpDownAsKeysLeftRight = true; + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + foreach (Key key in keys) + { + top.NewKeyDownEvent (key); + Application.MainLoop.RunIteration (); + } + + Assert.Equal (expectedBarTitle, mbiCurrent != null ? mbiCurrent.Title : "Closed"); + Assert.Equal (expectedItemTitle, miCurrent != null ? miCurrent.Title : "None"); + Assert.Equal (expectedAction, miAction); + top.Dispose (); + } + + [Theory] + [AutoInitShutdown] + [InlineData ("New", KeyCode.CtrlMask | KeyCode.N)] + [InlineData ("Quit", KeyCode.CtrlMask | KeyCode.Q)] + [InlineData ("Copy", KeyCode.CtrlMask | KeyCode.C)] + [InlineData ("Replace", KeyCode.CtrlMask | KeyCode.H)] + [InlineData ("1", KeyCode.F1)] + [InlineData ("5", KeyCode.CtrlMask | KeyCode.D5)] + public void KeyBindings_Shortcut_Commands (string expectedAction, params KeyCode [] keys) + { + var miAction = ""; + MenuItem mbiCurrent = null; + MenuItem miCurrent = null; + + var menu = new MenuBar (); + + bool FnAction (string s) + { + miAction = s; + + return true; + } + + // Declare a variable for the function + Func fnActionVariable = FnAction; + + menu.EnableForDesign (ref fnActionVariable); + + menu.Key = KeyCode.F9; + menu.MenuOpening += (s, e) => mbiCurrent = e.CurrentMenu; + menu.MenuOpened += (s, e) => { miCurrent = e.MenuItem; }; + + menu.MenuClosing += (s, e) => + { + mbiCurrent = null; + miCurrent = null; + }; + menu.UseKeysUpDownAsKeysLeftRight = true; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + foreach (KeyCode key in keys) + { + Assert.True (top.NewKeyDownEvent (new (key))); + Application.MainLoop!.RunIteration (); + } + + Assert.Equal (expectedAction, miAction); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void Menu_With_Separator () + { + var menu = new MenuBar + { + Menus = + [ + new ( + "File", + new MenuItem [] + { + new ( + "_Open", + "Open a file", + () => { }, + null, + null, + KeyCode.CtrlMask | KeyCode.O + ), + null, + new ("_Quit", "", null) + } + ) + ] + }; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + menu.OpenMenu (); + Application.LayoutAndDraw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" + File +┌────────────────────────────┐ +│ Open Open a file Ctrl+O │ +├────────────────────────────┤ +│ Quit │ +└────────────────────────────┘", + output + ); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void Menu_With_Separator_Disabled_Border () + { + var menu = new MenuBar + { + MenusBorderStyle = LineStyle.None, + Menus = + [ + new ( + "File", + new MenuItem [] + { + new ( + "_Open", + "Open a file", + () => { }, + null, + null, + KeyCode.CtrlMask | KeyCode.O + ), + null, + new ("_Quit", "", null) + } + ) + ] + }; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + menu.OpenMenu (); + Application.LayoutAndDraw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" + File + Open Open a file Ctrl+O +──────────────────────────── + Quit ", + output + ); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void MenuBar_ButtonPressed_Open_The_Menu_ButtonPressed_Again_Close_The_Menu () + { + // Define the expected menu + var expectedMenu = new ExpectedMenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("Open", "", null) }), + new ( + "Edit", + new MenuItem [] { new ("Copy", "", null) } + ) + ] + }; + + // Test without HotKeys first + var menu = new MenuBar + { + Menus = + [ + new ( + "_" + expectedMenu.Menus [0].Title, + new MenuItem [] { new ("_" + expectedMenu.Menus [0].Children [0].Title, "", null) } + ), + new ( + "_" + expectedMenu.Menus [1].Title, + new MenuItem [] + { + new ( + "_" + + expectedMenu.Menus [1] + .Children [0] + .Title, + "", + null + ) + } + ) + ] + }; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Assert.True (menu.NewMouseEvent (new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu })); + Assert.True (menu.IsMenuOpen); + top.Draw (); + + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); + + Assert.True (menu.NewMouseEvent (new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu })); + Assert.False (menu.IsMenuOpen); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void MenuBar_In_Window_Without_Other_Views_With_Top_Init () + { + var win = new Window (); + + var menu = new MenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("New", "", null) }), + new ( + "Edit", + new MenuItem [] + { + new MenuBarItem ( + "Delete", + new MenuItem [] + { new ("All", "", null), new ("Selected", "", null) } + ) + } + ) + ] + }; + win.Add (menu); + Toplevel top = new (); + top.Add (win); + Application.Begin (top); + ((FakeDriver)Application.Driver!).SetBufferSize (40, 8); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (win.NewKeyDownEvent (menu.Key)); + top.Draw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│┌──────┐ │ +││ New │ │ +│└──────┘ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (menu.NewKeyDownEvent (Key.CursorRight)); + Application.LayoutAndDraw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│ ┌─────────┐ │ +│ │ Delete ►│ │ +│ └─────────┘ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); + top.Draw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│ ┌─────────┐ │ +│ │ Delete ►│┌───────────┐ │ +│ └─────────┘│ All │ │ +│ │ Selected │ │ +│ └───────────┘ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); + View.SetClipToScreen (); + top.Draw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│┌──────┐ │ +││ New │ │ +│└──────┘ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void MenuBar_In_Window_Without_Other_Views_With_Top_Init_With_Parameterless_Run () + { + var win = new Window (); + + var menu = new MenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("New", "", null) }), + new ( + "Edit", + new MenuItem [] + { + new MenuBarItem ( + "Delete", + new MenuItem [] + { new ("All", "", null), new ("Selected", "", null) } + ) + } + ) + ] + }; + win.Add (menu); + Toplevel top = new (); + top.Add (win); + + Application.Iteration += (s, a) => + { + ((FakeDriver)Application.Driver!).SetBufferSize (40, 8); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (win.NewKeyDownEvent (menu.Key)); + top.Draw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│┌──────┐ │ +││ New │ │ +│└──────┘ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (menu.NewKeyDownEvent (Key.CursorRight)); + Application.LayoutAndDraw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│ ┌─────────┐ │ +│ │ Delete ►│ │ +│ └─────────┘ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); + top.Draw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│ ┌─────────┐ │ +│ │ Delete ►│┌───────────┐ │ +│ └─────────┘│ All │ │ +│ │ Selected │ │ +│ └───────────┘ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); + View.SetClipToScreen (); + top.Draw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│┌──────┐ │ +││ New │ │ +│└──────┘ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Application.RequestStop (); + }; + + Application.Run (top); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void MenuBar_In_Window_Without_Other_Views_Without_Top_Init () + { + var win = new Window (); + + var menu = new MenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("New", "", null) }), + new ( + "Edit", + new MenuItem [] + { + new MenuBarItem ( + "Delete", + new MenuItem [] + { new ("All", "", null), new ("Selected", "", null) } + ) + } + ) + ] + }; + win.Add (menu); + ((FakeDriver)Application.Driver!).SetBufferSize (40, 8); + RunState rs = Application.Begin (win); + Application.RunIteration (ref rs); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (win.NewKeyDownEvent (menu.Key)); + Application.RunIteration (ref rs); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│┌──────┐ │ +││ New │ │ +│└──────┘ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (menu.NewKeyDownEvent (Key.CursorRight)); + Application.RunIteration (ref rs); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│ ┌─────────┐ │ +│ │ Delete ►│ │ +│ └─────────┘ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); + Application.RunIteration (ref rs); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│ ┌─────────┐ │ +│ │ Delete ►│┌───────────┐ │ +│ └─────────┘│ All │ │ +│ │ Selected │ │ +│ └───────────┘ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); + Application.RunIteration (ref rs); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│┌──────┐ │ +││ New │ │ +│└──────┘ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + win.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void MenuBar_In_Window_Without_Other_Views_Without_Top_Init_With_Run_T () + { + ((FakeDriver)Application.Driver!).SetBufferSize (40, 8); + + Application.Iteration += (s, a) => + { + Toplevel top = Application.Top; + Application.LayoutAndDraw(); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (top.NewKeyDownEvent (Key.F9)); + top.Draw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│┌──────┐ │ +││ New │ │ +│└──────┘ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True (top.SubViews.ElementAt (0).NewKeyDownEvent (Key.CursorRight)); + Application.LayoutAndDraw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│ ┌─────────┐ │ +│ │ Delete ►│ │ +│ └─────────┘ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True ( + ((MenuBar)top.SubViews.ElementAt (0))._openMenu.NewKeyDownEvent (Key.CursorRight) + ); + top.Draw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│ ┌─────────┐ │ +│ │ Delete ►│┌───────────┐ │ +│ └─────────┘│ All │ │ +│ │ Selected │ │ +│ └───────────┘ │ +└──────────────────────────────────────┘", + output + ); + + Assert.True ( + ((MenuBar)top.SubViews.ElementAt (0))._openMenu.NewKeyDownEvent (Key.CursorRight) + ); + View.SetClipToScreen (); + top.Draw (); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" +┌──────────────────────────────────────┐ +│ File Edit │ +│┌──────┐ │ +││ New │ │ +│└──────┘ │ +│ │ +│ │ +└──────────────────────────────────────┘", + output + ); + + Application.RequestStop (); + }; + + Application.Run ().Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void MenuBar_Position_And_Size_With_HotKeys_Is_The_Same_As_Without_HotKeys () + { + // Define the expected menu + var expectedMenu = new ExpectedMenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("12", "", null) }), + new ( + "Edit", + new MenuItem [] { new ("Copy", "", null) } + ) + ] + }; + + // Test without HotKeys first + var menu = new MenuBar + { + Menus = + [ + new ( + expectedMenu.Menus [0].Title, + new MenuItem [] { new (expectedMenu.Menus [0].Children [0].Title, "", null) } + ), + new ( + expectedMenu.Menus [1].Title, + new MenuItem [] + { + new ( + expectedMenu.Menus [1].Children [0].Title, + "", + null + ) + } + ) + ] + }; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + // Open first + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.True (menu.IsMenuOpen); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); + + // Open second + Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorRight)); + Assert.True (menu.IsMenuOpen); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); + + // Close menu + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.False (menu.IsMenuOpen); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); + + top.Remove (menu); + + // Now test WITH HotKeys + menu = new () + { + Menus = + [ + new ( + "_" + expectedMenu.Menus [0].Title, + new MenuItem [] { new ("_" + expectedMenu.Menus [0].Children [0].Title, "", null) } + ), + new ( + "_" + expectedMenu.Menus [1].Title, + new MenuItem [] + { + new ( + "_" + expectedMenu.Menus [1].Children [0].Title, + "", + null + ) + } + ) + ] + }; + + top.Add (menu); + + // Open first + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.True (menu.IsMenuOpen); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); + + // Open second + Assert.True (top.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorRight)); + Assert.True (menu.IsMenuOpen); + View.SetClipToScreen (); + Application.Top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); + + // Close menu + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.False (menu.IsMenuOpen); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void MenuBar_Submenus_Alignment_Correct () + { + // Define the expected menu + var expectedMenu = new ExpectedMenuBar + { + Menus = + [ + new ( + "File", + new MenuItem [] + { + new ( + "Really Long Sub Menu", + "", + null + ) + } + ), + new ( + "123", + new MenuItem [] { new ("Copy", "", null) } + ), + new ( + "Format", + new MenuItem [] { new ("Word Wrap", "", null) } + ), + new ( + "Help", + new MenuItem [] { new ("About", "", null) } + ), + new ( + "1", + new MenuItem [] { new ("2", "", null) } + ), + new ( + "3", + new MenuItem [] { new ("2", "", null) } + ), + new ( + "Last one", + new MenuItem [] { new ("Test", "", null) } + ) + ] + }; + + MenuBarItem [] items = new MenuBarItem [expectedMenu.Menus.Length]; + + for (var i = 0; i < expectedMenu.Menus.Length; i++) + { + items [i] = new ( + expectedMenu.Menus [i].Title, + new MenuItem [] { new (expectedMenu.Menus [i].Children [0].Title, "", null) } + ); + } + + var menu = new MenuBar { Menus = items }; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); + + for (var i = 0; i < expectedMenu.Menus.Length; i++) + { + menu.OpenMenu (i); + Assert.True (menu.IsMenuOpen); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (i), output); + } + + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void MenuBar_With_Action_But_Without_MenuItems_Not_Throw () + { + var menu = new MenuBar + { + Menus = + [ + new () { Title = "Test 1", Action = () => { } }, + + new () { Title = "Test 2", Action = () => { } } + ] + }; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + +#if SUPPORT_ALT_TO_ACTIVATE_MENU + Assert.True ( + Application.OnKeyUp ( + new KeyEventArgs ( + Key.AltMask + ) + ) + ); // changed to true because Alt activates menu bar +#endif + Assert.True (menu.NewKeyDownEvent (Key.CursorRight)); + Assert.True (menu.NewKeyDownEvent (Key.CursorRight)); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void MenuBarItem_Children_Null_Does_Not_Throw () + { + var menu = new MenuBar + { + Menus = + [ + new ("Test", "", null) + ] + }; + var top = new Toplevel (); + top.Add (menu); + + Exception exception = Record.Exception (() => menu.NewKeyDownEvent (Key.Space)); + Assert.Null (exception); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void MenuOpened_On_Disabled_MenuItem () + { + MenuItem parent = null; + MenuItem miCurrent = null; + Menu mCurrent = null; + + var menu = new MenuBar + { + Menus = + [ + new ( + "_File", + new MenuItem [] + { + new MenuBarItem ( + "_New", + new MenuItem [] + { + new ( + "_New doc", + "Creates new doc.", + null, + () => false + ) + } + ), + null, + new ("_Save", "Saves the file.", null) + } + ) + ] + }; + + menu.MenuOpened += (s, e) => + { + parent = e.Parent; + miCurrent = e.MenuItem; + mCurrent = menu._openMenu; + }; + menu.UseKeysUpDownAsKeysLeftRight = true; + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + // open the menu + Assert.True ( + menu.NewMouseEvent ( + new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } + ) + ); + Assert.True (menu.IsMenuOpen); + Assert.Equal ("_File", parent.Title); + Assert.Equal ("_File", miCurrent.Parent.Title); + Assert.Equal ("_New", miCurrent.Title); + + Assert.True ( + mCurrent.NewMouseEvent ( + new () { Position = new (1, 1), Flags = MouseFlags.ReportMousePosition, View = mCurrent } + ) + ); + Assert.True (menu.IsMenuOpen); + Assert.Equal ("_File", parent.Title); + Assert.Equal ("_File", miCurrent.Parent.Title); + Assert.Equal ("_New", miCurrent.Title); + + Assert.True ( + mCurrent.NewMouseEvent ( + new () { Position = new (1, 1), Flags = MouseFlags.ReportMousePosition, View = mCurrent } + ) + ); + Assert.True (menu.IsMenuOpen); + Assert.Equal ("_File", parent.Title); + Assert.Equal ("_File", miCurrent.Parent.Title); + Assert.Equal ("_New", miCurrent.Title); + + Assert.True ( + mCurrent.NewMouseEvent ( + new () { Position = new (1, 2), Flags = MouseFlags.ReportMousePosition, View = mCurrent } + ) + ); + Assert.True (menu.IsMenuOpen); + Assert.Equal ("_File", parent.Title); + Assert.Equal ("_File", miCurrent.Parent.Title); + Assert.Equal ("_Save", miCurrent.Title); + + // close the menu + Assert.True ( + menu.NewMouseEvent ( + new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } + ) + ); + Assert.False (menu.IsMenuOpen); + + // open the menu + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.True (menu.IsMenuOpen); + + // The _New doc is enabled but the sub-menu isn't enabled. Is show but can't be selected and executed + Assert.Equal ("_New", parent.Title); + Assert.Equal ("_New", miCurrent.Parent.Title); + Assert.Equal ("_New doc", miCurrent.Title); + + Assert.True (mCurrent.NewKeyDownEvent (Key.CursorDown)); + Assert.True (menu.IsMenuOpen); + Assert.Equal ("_File", parent.Title); + Assert.Equal ("_File", miCurrent.Parent.Title); + Assert.Equal ("_Save", miCurrent.Title); + + Assert.True (mCurrent.NewKeyDownEvent (Key.CursorUp)); + Assert.True (menu.IsMenuOpen); + Assert.Equal ("_File", parent.Title); + Assert.Null (miCurrent); + + // close the menu + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.False (menu.IsMenuOpen); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void MenuOpening_MenuOpened_MenuClosing_Events () + { + var miAction = ""; + var isMenuClosed = true; + var cancelClosing = false; + + var menu = new MenuBar + { + Menus = + [ + new ("_File", new MenuItem [] { new ("_New", "Creates new file.", New) }) + ] + }; + + menu.MenuOpening += (s, e) => + { + Assert.Equal ("_File", e.CurrentMenu.Title); + Assert.Equal ("_New", e.CurrentMenu.Children [0].Title); + Assert.Equal ("Creates new file.", e.CurrentMenu.Children [0].Help); + Assert.Equal (New, e.CurrentMenu.Children [0].Action); + e.CurrentMenu.Children [0].Action (); + Assert.Equal ("New", miAction); + + e.NewMenuBarItem = new ( + "_Edit", + new MenuItem [] { new ("_Copy", "Copies the selection.", Copy) } + ); + }; + + menu.MenuOpened += (s, e) => + { + MenuItem mi = e.MenuItem; + + Assert.Equal ("_Edit", mi.Parent.Title); + Assert.Equal ("_Copy", mi.Title); + Assert.Equal ("Copies the selection.", mi.Help); + Assert.Equal (Copy, mi.Action); + mi.Action (); + Assert.Equal ("Copy", miAction); + }; + + menu.MenuClosing += (s, e) => + { + Assert.False (isMenuClosed); + + if (cancelClosing) + { + e.Cancel = true; + isMenuClosed = false; + } + else + { + isMenuClosed = true; + } + }; + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.True (menu.IsMenuOpen); + isMenuClosed = !menu.IsMenuOpen; + Assert.False (isMenuClosed); + top.Draw (); + + var expected = @" +Edit +┌──────────────────────────────┐ +│ Copy Copies the selection. │ +└──────────────────────────────┘ +"; + DriverAssert.AssertDriverContentsAre (expected, output); + + cancelClosing = true; + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.True (menu.IsMenuOpen); + Assert.False (isMenuClosed); + View.SetClipToScreen (); + top.Draw (); + + expected = @" +Edit +┌──────────────────────────────┐ +│ Copy Copies the selection. │ +└──────────────────────────────┘ +"; + DriverAssert.AssertDriverContentsAre (expected, output); + + cancelClosing = false; + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.False (menu.IsMenuOpen); + Assert.True (isMenuClosed); + View.SetClipToScreen (); + top.Draw (); + + expected = @" +Edit +"; + DriverAssert.AssertDriverContentsAre (expected, output); + + void New () { miAction = "New"; } + + void Copy () { miAction = "Copy"; } + + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void MouseEvent_Test () + { + MenuItem miCurrent = null; + Menu mCurrent = null; + + var menuBar = new MenuBar + { + Menus = + [ + new ( + "_File", + new MenuItem [] { new ("_New", "", null), new ("_Open", "", null), new ("_Save", "", null) } + ), + new ( + "_Edit", + new MenuItem [] { new ("_Copy", "", null), new ("C_ut", "", null), new ("_Paste", "", null) } + ) + ] + }; + + menuBar.MenuOpened += (s, e) => + { + miCurrent = e.MenuItem; + mCurrent = menuBar.OpenCurrentMenu; + }; + var top = new Toplevel (); + top.Add (menuBar); + Application.Begin (top); + + // Click on Edit + Assert.True ( + menuBar.NewMouseEvent ( + new () { Position = new (10, 0), Flags = MouseFlags.Button1Pressed, View = menuBar } + ) + ); + Assert.True (menuBar.IsMenuOpen); + Assert.Equal ("_Edit", miCurrent.Parent.Title); + Assert.Equal ("_Copy", miCurrent.Title); + + // Click on Paste + Assert.True ( + mCurrent.NewMouseEvent ( + new () { Position = new (10, 2), Flags = MouseFlags.ReportMousePosition, View = mCurrent } + ) + ); + Assert.True (menuBar.IsMenuOpen); + Assert.Equal ("_Edit", miCurrent.Parent.Title); + Assert.Equal ("_Paste", miCurrent.Title); + + for (var i = 4; i >= -1; i--) + { + Application.RaiseMouseEvent ( + new () { ScreenPosition = new (10, i), Flags = MouseFlags.ReportMousePosition } + ); + + Assert.True (menuBar.IsMenuOpen); + Menu menu = (Menu)top.SubViews.First (v => v is Menu); + + if (i is < 0 or > 0) + { + Assert.Equal (menu, Application.MouseGrabView); + } + else + { + Assert.Equal (menuBar, Application.MouseGrabView); + } + + Assert.Equal ("_Edit", miCurrent.Parent.Title); + + if (i == 4) + { + Assert.Equal ("_Paste", miCurrent.Title); + } + else if (i == 3) + { + Assert.Equal ("C_ut", miCurrent.Title); + } + else if (i == 2) + { + Assert.Equal ("_Copy", miCurrent.Title); + } + else + { + Assert.Equal ("_Copy", miCurrent.Title); + } + } + + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void Parent_MenuItem_Stay_Focused_If_Child_MenuItem_Is_Empty_By_Keyboard () + { + var expectedMenu = new ExpectedMenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("New", "", null) }), + new ("Edit", Array.Empty ()), + new ( + "Format", + new MenuItem [] { new ("Wrap", "", null) } + ) + ] + }; + + MenuBarItem [] items = new MenuBarItem [expectedMenu.Menus.Length]; + + for (var i = 0; i < expectedMenu.Menus.Length; i++) + { + items [i] = new ( + expectedMenu.Menus [i].Title, + expectedMenu.Menus [i].Children.Length > 0 + ? new MenuItem [] { new (expectedMenu.Menus [i].Children [0].Title, "", null) } + : Array.Empty () + ); + } + + var menu = new MenuBar { Menus = items }; + + var tf = new TextField { Y = 2, Width = 10 }; + var top = new Toplevel (); + top.Add (menu, tf); + + Application.Begin (top); + Assert.True (tf.HasFocus); + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.True (menu.IsMenuOpen); + Assert.False (tf.HasFocus); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); + + // Right - Edit has no sub menu; this tests that no sub menu shows + Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); + Assert.True (menu.IsMenuOpen); + Assert.False (tf.HasFocus); + Assert.Equal (1, menu._selected); + Assert.Equal (-1, menu._selectedSub); + Assert.Null (menu._openSubMenu); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); + + // Right - Format + Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorRight)); + Assert.True (menu.IsMenuOpen); + Assert.False (tf.HasFocus); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (2), output); + + // Left - Edit + Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorLeft)); + Assert.True (menu.IsMenuOpen); + Assert.False (tf.HasFocus); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); + + Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorLeft)); + Assert.True (menu.IsMenuOpen); + Assert.False (tf.HasFocus); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); + + Assert.True (Application.RaiseKeyDownEvent (menu.Key)); + Assert.False (menu.IsMenuOpen); + Assert.True (tf.HasFocus); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void Parent_MenuItem_Stay_Focused_If_Child_MenuItem_Is_Empty_By_Mouse () + { + // File Edit Format + //┌──────┐ ┌───────┐ + //│ New │ │ Wrap │ + //└──────┘ └───────┘ + + // Define the expected menu + var expectedMenu = new ExpectedMenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("New", "", null) }), + new ("Edit", new MenuItem [] { }), + new ( + "Format", + new MenuItem [] { new ("Wrap", "", null) } + ) + ] + }; + + var menu = new MenuBar + { + Menus = + [ + new ( + expectedMenu.Menus [0].Title, + new MenuItem [] { new (expectedMenu.Menus [0].Children [0].Title, "", null) } + ), + new (expectedMenu.Menus [1].Title, new MenuItem [] { }), + new ( + expectedMenu.Menus [2].Title, + new MenuItem [] + { + new ( + expectedMenu.Menus [2].Children [0].Title, + "", + null + ) + } + ) + ] + }; + + var tf = new TextField { Y = 2, Width = 10 }; + var top = new Toplevel (); + top.Add (menu, tf); + Application.Begin (top); + + Assert.True (tf.HasFocus); + Assert.True (menu.NewMouseEvent (new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu })); + Assert.True (menu.IsMenuOpen); + Assert.False (tf.HasFocus); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); + + Assert.True ( + menu.NewMouseEvent ( + new () { Position = new (8, 0), Flags = MouseFlags.ReportMousePosition, View = menu } + ) + ); + Assert.True (menu.IsMenuOpen); + Assert.False (tf.HasFocus); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (1), output); + + Assert.True ( + menu.NewMouseEvent ( + new () { Position = new (15, 0), Flags = MouseFlags.ReportMousePosition, View = menu } + ) + ); + Assert.True (menu.IsMenuOpen); + Assert.False (tf.HasFocus); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (2), output); + + Assert.True ( + menu.NewMouseEvent ( + new () { Position = new (8, 0), Flags = MouseFlags.ReportMousePosition, View = menu } + ) + ); + Assert.True (menu.IsMenuOpen); + Assert.False (tf.HasFocus); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); + + Assert.True ( + menu.NewMouseEvent ( + new () { Position = new (1, 0), Flags = MouseFlags.ReportMousePosition, View = menu } + ) + ); + Assert.True (menu.IsMenuOpen); + Assert.False (tf.HasFocus); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ExpectedSubMenuOpen (0), output); + + Assert.True (menu.NewMouseEvent (new () { Position = new (8, 0), Flags = MouseFlags.Button1Pressed, View = menu })); + Assert.False (menu.IsMenuOpen); + Assert.True (tf.HasFocus); + View.SetClipToScreen (); + top.Draw (); + DriverAssert.AssertDriverContentsAre (expectedMenu.ClosedMenuText, output); + top.Dispose (); + } + + [Fact] + public void RemoveAndThenAddMenuBar_ShouldNotChangeWidth () + { + MenuBar menuBar; + MenuBar menuBar2; + + // TODO: When https: //github.com/gui-cs/Terminal.Gui/issues/3136 is fixed, + // TODO: Change this to Window + var w = new View (); + menuBar2 = new (); + menuBar = new (); + w.Width = Dim.Fill (); + w.Height = Dim.Fill (); + w.X = 0; + w.Y = 0; + + w.Visible = true; + + // TODO: When https: //github.com/gui-cs/Terminal.Gui/issues/3136 is fixed, + // TODO: uncomment this. + //w.Modal = false; + w.Title = ""; + menuBar.Width = Dim.Fill (); + menuBar.Height = 1; + menuBar.X = 0; + menuBar.Y = 0; + menuBar.Visible = true; + w.Add (menuBar); + + menuBar2.Width = Dim.Fill (); + menuBar2.Height = 1; + menuBar2.X = 0; + menuBar2.Y = 4; + menuBar2.Visible = true; + w.Add (menuBar2); + + MenuBar [] menuBars = w.SubViews.OfType ().ToArray (); + Assert.Equal (2, menuBars.Length); + + Assert.Equal (Dim.Fill (), menuBars [0].Width); + Assert.Equal (Dim.Fill (), menuBars [1].Width); + + // Goes wrong here + w.Remove (menuBar); + w.Remove (menuBar2); + + w.Add (menuBar); + w.Add (menuBar2); + + // These assertions fail + Assert.Equal (Dim.Fill (), menuBars [0].Width); + Assert.Equal (Dim.Fill (), menuBars [1].Width); + } + + [Fact] + [AutoInitShutdown] + public void Resizing_Close_Menus () + { + var menu = new MenuBar + { + Menus = + [ + new ( + "File", + new MenuItem [] + { + new ( + "Open", + "Open a file", + () => { }, + null, + null, + KeyCode.CtrlMask | KeyCode.O + ) + } + ) + ] + }; + var top = new Toplevel (); + top.Add (menu); + RunState rs = Application.Begin (top); + + menu.OpenMenu (); + var firstIteration = false; + Application.RunIteration (ref rs, firstIteration); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" + File +┌────────────────────────────┐ +│ Open Open a file Ctrl+O │ +└────────────────────────────┘", + output + ); + + ((FakeDriver)Application.Driver!).SetBufferSize (20, 15); + firstIteration = false; + Application.RunIteration (ref rs, firstIteration); + + DriverAssert.AssertDriverContentsWithFrameAre ( + @" + File", + output + ); + + Application.End (rs); + top.Dispose (); + } + + [Fact] + public void Separator_Does_Not_Throws_Pressing_Menu_Hotkey () + { + var menu = new MenuBar + { + Menus = + [ + new ( + "File", + new MenuItem [] { new ("_New", "", null), null, new ("_Quit", "", null) } + ) + ] + }; + Assert.False (menu.NewKeyDownEvent (Key.Q.WithAlt)); + } + + [Fact] + public void SetMenus_With_Same_HotKey_Does_Not_Throws () + { + var mb = new MenuBar (); + + var i1 = new MenuBarItem ("_heey", "fff", () => { }, () => true); + + mb.Menus = new [] { i1 }; + mb.Menus = new [] { i1 }; + + Assert.Equal (Key.H, mb.Menus [0].HotKey); + } + + [Fact] + [AutoInitShutdown] + public void ShortCut_Activates () + { + var saveAction = false; + + var menu = new MenuBar + { + Menus = + [ + new ( + "_File", + new MenuItem [] + { + new ( + "_Save", + "Saves the file.", + () => { saveAction = true; }, + null, + null, + (KeyCode)Key.S.WithCtrl + ) + } + ) + ] + }; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Application.RaiseKeyDownEvent (Key.S.WithCtrl); + Application.MainLoop.RunIteration (); + + Assert.True (saveAction); + top.Dispose (); + } + + [Fact] + public void Update_ShortcutKey_KeyBindings_Old_ShortcutKey_Is_Removed () + { + var menuBar = new MenuBar + { + Menus = + [ + new ( + "_File", + new MenuItem [] + { + new ("New", "Create New", null, null, null, Key.A.WithCtrl) + } + ) + ] + }; + + Assert.True (menuBar.HotKeyBindings.TryGet (Key.A.WithCtrl, out _)); + + menuBar.Menus [0].Children! [0].ShortcutKey = Key.B.WithCtrl; + + Assert.False (menuBar.HotKeyBindings.TryGet (Key.A.WithCtrl, out _)); + Assert.True (menuBar.HotKeyBindings.TryGet (Key.B.WithCtrl, out _)); + } + + [Fact] + public void UseKeysUpDownAsKeysLeftRight_And_UseSubMenusSingleFrame_Cannot_Be_Both_True () + { + var menu = new MenuBar (); + Assert.False (menu.UseKeysUpDownAsKeysLeftRight); + Assert.False (menu.UseSubMenusSingleFrame); + + menu.UseKeysUpDownAsKeysLeftRight = true; + Assert.True (menu.UseKeysUpDownAsKeysLeftRight); + Assert.False (menu.UseSubMenusSingleFrame); + + menu.UseSubMenusSingleFrame = true; + Assert.False (menu.UseKeysUpDownAsKeysLeftRight); + Assert.True (menu.UseSubMenusSingleFrame); + } + + [Fact (Skip = "#3798 Broke. Will fix in #2975")] + [AutoInitShutdown] + public void UseSubMenusSingleFrame_False_By_Keyboard () + { + var menu = new MenuBar + { + Menus = new MenuBarItem [] + { + new ( + "Numbers", + new MenuItem [] + { + new ("One", "", null), + new MenuBarItem ( + "Two", + new MenuItem [] + { + new ("Sub-Menu 1", "", null), + new ("Sub-Menu 2", "", null) + } + ), + new ("Three", "", null) + } + ) + } + }; + menu.UseKeysUpDownAsKeysLeftRight = true; + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); + Assert.False (menu.UseSubMenusSingleFrame); + + top.Draw (); + + var expected = @" + Numbers +"; + + Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + + Assert.True (menu.NewKeyDownEvent (menu.Key)); + top.Draw (); + + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│ +│ Three │ +└────────┘ +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + + Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorDown)); + top.Draw (); + + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│┌─────────────┐ +│ Three ││ Sub-Menu 1 │ +└────────┘│ Sub-Menu 2 │ + └─────────────┘ +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + + Assert.True (Application.Top.SubViews.ElementAt (2).NewKeyDownEvent (Key.CursorLeft)); + top.Draw (); + + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│ +│ Three │ +└────────┘ +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + + Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.Esc)); + top.Draw (); + + expected = @" + Numbers +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + top.Dispose (); + } + + [Fact (Skip = "#3798 Broke. Will fix in #2975")] + [AutoInitShutdown] + public void UseSubMenusSingleFrame_False_By_Mouse () + { + var menu = new MenuBar + { + Menus = + [ + new ( + "Numbers", + new MenuItem [] + { + new ("One", "", null), + new MenuBarItem ( + "Two", + new MenuItem [] + { + new ( + "Sub-Menu 1", + "", + null + ), + new ( + "Sub-Menu 2", + "", + null + ) + } + ), + new ("Three", "", null) + } + ) + ] + }; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); + Assert.False (menu.UseSubMenusSingleFrame); + + top.Draw (); + + var expected = @" + Numbers +"; + + Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 8, 1), pos); + + menu.NewMouseEvent ( + new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } + ); + top.Draw (); + + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│ +│ Three │ +└────────┘ +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 10, 6), pos); + + menu.NewMouseEvent ( + new () + { + Position = new (1, 2), Flags = MouseFlags.ReportMousePosition, View = Application.Top.SubViews.ElementAt (1) + } + ); + top.Draw (); + + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│┌─────────────┐ +│ Three ││ Sub-Menu 1 │ +└────────┘│ Sub-Menu 2 │ + └─────────────┘ +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 25, 7), pos); + + Assert.False ( + menu.NewMouseEvent ( + new () + { + Position = new (1, 1), Flags = MouseFlags.ReportMousePosition, View = Application.Top.SubViews.ElementAt (1) + } + ) + ); + top.Draw (); + + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│ +│ Three │ +└────────┘ +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 10, 6), pos); + + menu.NewMouseEvent ( + new () { Position = new (70, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top } + ); + top.Draw (); + + expected = @" + Numbers +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 8, 1), pos); + top.Dispose (); + } + + [Fact (Skip = "#3798 Broke. Will fix in #2975")] + [AutoInitShutdown] + public void UseSubMenusSingleFrame_False_Disabled_Border () + { + var menu = new MenuBar + { + MenusBorderStyle = LineStyle.None, + Menus = + [ + new ( + "Numbers", + new MenuItem [] + { + new ("One", "", null), + new MenuBarItem ( + "Two", + new MenuItem [] + { + new ( + "Sub-Menu 1", + "", + null + ), + new ( + "Sub-Menu 2", + "", + null + ) + } + ), + new ("Three", "", null) + } + ) + ] + }; + + menu.UseKeysUpDownAsKeysLeftRight = true; + menu.BeginInit (); + menu.EndInit (); + + menu.OpenMenu (); + menu.ColorScheme = menu._openMenu.ColorScheme = new (Attribute.Default); + Assert.True (menu.IsMenuOpen); + + menu.Draw (); + menu._openMenu.Draw (); + + var expected = @" + Numbers + One + Two ► + Three "; + + _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + + Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorDown)); + menu.Draw (); + menu._openMenu.Draw (); + menu.OpenCurrentMenu.Draw (); + + expected = @" + Numbers + One + Two ► Sub-Menu 1 + Three Sub-Menu 2"; + + _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + } + + [Fact (Skip = "#3798 Broke. Will fix in #2975")] + [AutoInitShutdown] + public void UseSubMenusSingleFrame_True_By_Keyboard () + { + var menu = new MenuBar + { + Menus = + [ + new ( + "Numbers", + new MenuItem [] + { + new ("One", "", null), + new MenuBarItem ( + "Two", + new MenuItem [] + { + new ( + "Sub-Menu 1", + "", + null + ), + new ( + "Sub-Menu 2", + "", + null + ) + } + ), + new ("Three", "", null) + } + ) + ] + }; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); + Assert.False (menu.UseSubMenusSingleFrame); + menu.UseSubMenusSingleFrame = true; + Assert.True (menu.UseSubMenusSingleFrame); + + top.Draw (); + + var expected = @" + Numbers +"; + + Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 8, 1), pos); + + Assert.True (menu.NewKeyDownEvent (menu.Key)); + top.Draw (); + + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│ +│ Three │ +└────────┘ +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 10, 6), pos); + + Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.CursorDown)); + Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.Enter)); + top.Draw (); + + expected = @" + Numbers +┌─────────────┐ +│◄ Two │ +├─────────────┤ +│ Sub-Menu 1 │ +│ Sub-Menu 2 │ +└─────────────┘ +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 15, 7), pos); + + Assert.True (Application.Top.SubViews.ElementAt (2).NewKeyDownEvent (Key.Enter)); + top.Draw (); + + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│ +│ Three │ +└────────┘ +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 10, 6), pos); + + Assert.True (Application.Top.SubViews.ElementAt (1).NewKeyDownEvent (Key.Esc)); + top.Draw (); + + expected = @" + Numbers +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 8, 1), pos); + top.Dispose (); + } + + [Fact (Skip = "#3798 Broke. Will fix in #2975")] + [AutoInitShutdown] + public void UseSubMenusSingleFrame_True_By_Mouse () + { + var menu = new MenuBar + { + Menus = + [ + new ( + "Numbers", + new MenuItem [] + { + new ("One", "", null), + new MenuBarItem ( + "Two", + new MenuItem [] + { + new ( + "Sub-Menu 1", + "", + null + ), + new ( + "Sub-Menu 2", + "", + null + ) + } + ), + new ("Three", "", null) + } + ) + ] + }; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); + Assert.False (menu.UseSubMenusSingleFrame); + menu.UseSubMenusSingleFrame = true; + Assert.True (menu.UseSubMenusSingleFrame); + + top.Draw (); + + var expected = @" + Numbers +"; + + Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 8, 1), pos); + + Assert.True (menu.NewMouseEvent (new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu })); + top.Draw (); + + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│ +│ Three │ +└────────┘ +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 10, 6), pos); + + Assert.False (menu.NewMouseEvent (new () { Position = new (1, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top.SubViews.ElementAt (1) })); + top.Draw (); + + expected = @" + Numbers +┌─────────────┐ +│◄ Two │ +├─────────────┤ +│ Sub-Menu 1 │ +│ Sub-Menu 2 │ +└─────────────┘ +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 15, 7), pos); + + menu.NewMouseEvent (new () { Position = new (1, 1), Flags = MouseFlags.Button1Clicked, View = Application.Top.SubViews.ElementAt (2) }); + top.Draw (); + + expected = @" + Numbers +┌────────┐ +│ One │ +│ Two ►│ +│ Three │ +└────────┘ +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 10, 6), pos); + + Assert.False (menu.NewMouseEvent (new () { Position = new (70, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top })); + top.Draw (); + + expected = @" + Numbers +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 8, 1), pos); + top.Dispose (); + } + + [Fact (Skip = "#3798 Broke. Will fix in #2975")] + [AutoInitShutdown] + public void UseSubMenusSingleFrame_True_Disabled_Border () + { + var menu = new MenuBar + { + MenusBorderStyle = LineStyle.None, + Menus = + [ + new ( + "Numbers", + new MenuItem [] + { + new ("One", "", null), + new MenuBarItem ( + "Two", + new MenuItem [] + { + new ( + "Sub-Menu 1", + "", + null + ), + new ( + "Sub-Menu 2", + "", + null + ) + } + ), + new ("Three", "", null) + } + ) + ] + }; + + menu.UseSubMenusSingleFrame = true; + menu.BeginInit (); + menu.EndInit (); + + menu.OpenMenu (); + Assert.True (menu.IsMenuOpen); + + menu.Draw (); + menu.ColorScheme = menu._openMenu.ColorScheme = new (Attribute.Default); + menu._openMenu.Draw (); + + var expected = @" + Numbers + One + Two ► + Three "; + + _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + + Assert.True (menu._openMenu.NewKeyDownEvent (Key.CursorDown)); + Assert.True (menu._openMenu.NewKeyDownEvent (Key.Enter)); + menu.Draw (); + menu._openMenu.Draw (); + menu.OpenCurrentMenu.Draw (); + + expected = @" + Numbers +◄ Two +───────────── + Sub-Menu 1 + Sub-Menu 2 "; + + _ = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + } + + [Fact (Skip = "#3798 Broke. Will fix in #2975")] + [AutoInitShutdown] + public void UseSubMenusSingleFrame_True_Without_Border () + { + var menu = new MenuBar + { + UseSubMenusSingleFrame = true, + MenusBorderStyle = LineStyle.None, + Menus = + [ + new ( + "Numbers", + new MenuItem [] + { + new ("One", "", null), + new MenuBarItem ( + "Two", + new MenuItem [] + { + new ( + "Sub-Menu 1", + "", + null + ), + new ( + "Sub-Menu 2", + "", + null + ) + } + ), + new ("Three", "", null) + } + ) + ] + }; + + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Assert.Equal (Point.Empty, new (menu.Frame.X, menu.Frame.Y)); + Assert.True (menu.UseSubMenusSingleFrame); + Assert.Equal (LineStyle.None, menu.MenusBorderStyle); + + top.Draw (); + + var expected = @" + Numbers +"; + + Rectangle pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 8, 1), pos); + + Assert.True ( + menu.NewMouseEvent ( + new () { Position = new (1, 0), Flags = MouseFlags.Button1Pressed, View = menu } + ) + ); + top.Draw (); + + expected = @" + Numbers + One + Two ► + Three +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 8, 4), pos); + + menu.NewMouseEvent ( + new () { Position = new (1, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top.SubViews.ElementAt (1) } + ); + top.Draw (); + + expected = @" + Numbers +◄ Two +───────────── + Sub-Menu 1 + Sub-Menu 2 +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 13, 5), pos); + + menu.NewMouseEvent ( + new () { Position = new (1, 1), Flags = MouseFlags.Button1Clicked, View = Application.Top.SubViews.ElementAt (2) } + ); + top.Draw (); + + expected = @" + Numbers + One + Two ► + Three +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 8, 4), pos); + + menu.NewMouseEvent ( + new () { Position = new (70, 2), Flags = MouseFlags.Button1Clicked, View = Application.Top } + ); + top.Draw (); + + expected = @" + Numbers +"; + + pos = DriverAssert.AssertDriverContentsWithFrameAre (expected, output); + Assert.Equal (new (1, 0, 8, 1), pos); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void Visible_False_Key_Does_Not_Open_And_Close_All_Opened_Menus () + { + var menu = new MenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("New", "", null) }) + ] + }; + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Assert.True (menu.Visible); + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.True (menu.IsMenuOpen); + + menu.Visible = false; + Assert.False (menu.IsMenuOpen); + + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.False (menu.IsMenuOpen); + top.Dispose (); + } + + [Fact] + [AutoInitShutdown] + public void CanFocus_True_Key_Esc_Exit_Toplevel_If_IsMenuOpen_False () + { + var menu = new MenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("New", "", null) }) + ], + CanFocus = true + }; + var top = new Toplevel (); + top.Add (menu); + Application.Begin (top); + + Assert.True (menu.CanFocus); + Assert.True (menu.NewKeyDownEvent (menu.Key)); + Assert.True (menu.IsMenuOpen); + + Assert.True (menu.NewKeyDownEvent (Key.Esc)); + Assert.False (menu.IsMenuOpen); + + Assert.False (menu.NewKeyDownEvent (Key.Esc)); + Assert.False (menu.IsMenuOpen); + top.Dispose (); + } + + // Defines the expected strings for a Menu. Currently supports + // - MenuBar with any number of MenuItems + // - Each top-level MenuItem can have a SINGLE sub-menu + // + // TODO: Enable multiple sub-menus + // TODO: Enable checked sub-menus + // TODO: Enable sub-menus with sub-menus (perhaps better to put this in a separate class with focused unit tests?) + // + // E.g: + // + // File Edit + // New Copy + public class ExpectedMenuBar : MenuBar + { + private FakeDriver _d = (FakeDriver)Application.Driver; + + // The expected strings when the menu is closed + public string ClosedMenuText => MenuBarText + "\n"; + + public string ExpectedBottomRow (int i) + { + return $"{Glyphs.LLCorner}{new (Glyphs.HLine.ToString () [0], Menus [i].Children [0].TitleLength + 3)}{Glyphs.LRCorner} \n"; + } + + // The 3 spaces at end are a result of Menu.cs line 1062 where `pos` is calculated (` + spacesAfterTitle`) + public string ExpectedMenuItemRow (int i) { return $"{Glyphs.VLine} {Menus [i].Children [0].Title} {Glyphs.VLine} \n"; } + + // The full expected string for an open sub menu + public string ExpectedSubMenuOpen (int i) + { + return ClosedMenuText + + (Menus [i].Children.Length > 0 + ? ExpectedPadding (i) + + ExpectedTopRow (i) + + ExpectedPadding (i) + + ExpectedMenuItemRow (i) + + ExpectedPadding (i) + + ExpectedBottomRow (i) + : ""); + } + + // Define expected menu frame + // "┌──────┐" + // "│ New │" + // "└──────┘" + // + // The width of the Frame is determined in Menu.cs line 144, where `Width` is calculated + // 1 space before the Title and 2 spaces after the Title/Check/Help + public string ExpectedTopRow (int i) + { + return $"{Glyphs.ULCorner}{new (Glyphs.HLine.ToString () [0], Menus [i].Children [0].TitleLength + 3)}{Glyphs.URCorner} \n"; + } + + // Each MenuBar title has a 1 space pad on each side + // See `static int leftPadding` and `static int rightPadding` on line 1037 of Menu.cs + public string MenuBarText + { + get + { + var txt = string.Empty; + + foreach (MenuBarItem m in Menus) + { + txt += " " + m.Title + " "; + } + + return txt; + } + } + + // Padding for the X of the sub menu Frame + // Menu.cs - Line 1239 in `internal void OpenMenu` is where the Menu is created + private string ExpectedPadding (int i) + { + var n = 0; + + while (i > 0) + { + n += Menus [i - 1].TitleLength + 2; + i--; + } + + return new (' ', n); + } + } + + private class CustomWindow : Window + { + public CustomWindow () + { + var menu = new MenuBar + { + Menus = + [ + new ("File", new MenuItem [] { new ("New", "", null) }), + new ( + "Edit", + new MenuItem [] + { + new MenuBarItem ( + "Delete", + new MenuItem [] + { new ("All", "", null), new ("Selected", "", null) } + ) + } + ) + ] + }; + Add (menu); + } + } +} diff --git a/Tests/UnitTests/Views/MenuTests.cs b/Tests/UnitTests/Views/Menuv1/Menuv1Tests.cs similarity index 97% rename from Tests/UnitTests/Views/MenuTests.cs rename to Tests/UnitTests/Views/Menuv1/Menuv1Tests.cs index d00c4a3753..2c8386004a 100644 --- a/Tests/UnitTests/Views/MenuTests.cs +++ b/Tests/UnitTests/Views/Menuv1/Menuv1Tests.cs @@ -4,10 +4,10 @@ namespace Terminal.Gui.ViewsTests; -public class MenuTests +public class Menuv1Tests { private readonly ITestOutputHelper _output; - public MenuTests (ITestOutputHelper output) { _output = output; } + public Menuv1Tests (ITestOutputHelper output) { _output = output; } // TODO: Create more low-level unit tests for Menu and MenuItem diff --git a/Tests/UnitTestsParallelizable/Views/FlagSelectorTests.cs b/Tests/UnitTestsParallelizable/Views/FlagSelectorTests.cs new file mode 100644 index 0000000000..a4feda11d6 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Views/FlagSelectorTests.cs @@ -0,0 +1,124 @@ + +namespace Terminal.Gui.ViewsTests; + +public class FlagSelectorTests +{ + [Fact] + public void Initialization_ShouldSetDefaults() + { + var flagSelector = new FlagSelector(); + + Assert.True(flagSelector.CanFocus); + Assert.Equal(Dim.Auto(DimAutoStyle.Content), flagSelector.Width); + Assert.Equal(Dim.Auto(DimAutoStyle.Content), flagSelector.Height); + Assert.Equal(Orientation.Vertical, flagSelector.Orientation); + } + + [Fact] + public void SetFlags_WithDictionary_ShouldSetFlags() + { + var flagSelector = new FlagSelector(); + var flags = new Dictionary + { + { 1, "Flag1" }, + { 2, "Flag2" } + }; + + flagSelector.SetFlags(flags); + + Assert.Equal(flags, flagSelector.Flags); + } + + [Fact] + public void SetFlags_WithEnum_ShouldSetFlags() + { + var flagSelector = new FlagSelector(); + + flagSelector.SetFlags(); + + var expectedFlags = Enum.GetValues() + .ToDictionary(f => Convert.ToUInt32(f), f => f.ToString()); + + Assert.Equal(expectedFlags, flagSelector.Flags); + } + + [Fact] + public void SetFlags_WithEnumAndCustomNames_ShouldSetFlags() + { + var flagSelector = new FlagSelector(); + + flagSelector.SetFlags(f => f switch + { + FlagSelectorStyles.ShowNone => "Show None Value", + FlagSelectorStyles.ShowValueEdit => "Show Value Editor", + FlagSelectorStyles.All => "Everything", + _ => f.ToString() + }); + + var expectedFlags = Enum.GetValues() + .ToDictionary(f => Convert.ToUInt32(f), f => f switch + { + FlagSelectorStyles.ShowNone => "Show None Value", + FlagSelectorStyles.ShowValueEdit => "Show Value Editor", + FlagSelectorStyles.All => "Everything", + _ => f.ToString() + }); + + Assert.Equal(expectedFlags, flagSelector.Flags); + } + + [Fact] + public void Value_Set_ShouldUpdateCheckedState() + { + var flagSelector = new FlagSelector(); + var flags = new Dictionary + { + { 1, "Flag1" }, + { 2, "Flag2" } + }; + + flagSelector.SetFlags(flags); + flagSelector.Value = 1; + + var checkBox = flagSelector.SubViews.OfType().First(cb => (uint)cb.Data == 1); + Assert.Equal(CheckState.Checked, checkBox.CheckedState); + + checkBox = flagSelector.SubViews.OfType().First(cb => (uint)cb.Data == 2); + Assert.Equal(CheckState.UnChecked, checkBox.CheckedState); + } + + [Fact] + public void Styles_Set_ShouldCreateSubViews() + { + var flagSelector = new FlagSelector(); + var flags = new Dictionary + { + { 1, "Flag1" }, + { 2, "Flag2" } + }; + + flagSelector.SetFlags(flags); + flagSelector.Styles = FlagSelectorStyles.ShowNone; + + Assert.Contains(flagSelector.SubViews, sv => sv is CheckBox cb && cb.Title == "None"); + } + + [Fact] + public void ValueChanged_Event_ShouldBeRaised() + { + var flagSelector = new FlagSelector(); + var flags = new Dictionary + { + { 1, "Flag1" }, + { 2, "Flag2" } + }; + + flagSelector.SetFlags(flags); + bool eventRaised = false; + flagSelector.ValueChanged += (sender, args) => eventRaised = true; + + flagSelector.Value = 1; + + Assert.True(eventRaised); + } +} diff --git a/Tests/UnitTestsParallelizable/Views/MenuBarItemTests.cs b/Tests/UnitTestsParallelizable/Views/MenuBarItemTests.cs new file mode 100644 index 0000000000..238a39c64c --- /dev/null +++ b/Tests/UnitTestsParallelizable/Views/MenuBarItemTests.cs @@ -0,0 +1,22 @@ +using Xunit.Abstractions; + +//using static Terminal.Gui.ViewTests.MenuTests; + +namespace Terminal.Gui.ViewsTests; + +public class MenuBarItemTests () +{ + [Fact] + public void Constructors_Defaults () + { + var menuBarItem = new MenuBarItemv2 (); + Assert.Null (menuBarItem.PopoverMenu); + Assert.Null (menuBarItem.TargetView); + + menuBarItem = new MenuBarItemv2 (targetView: null, command: Command.NotBound, commandText: null, popoverMenu: null); + Assert.Null (menuBarItem.PopoverMenu); + Assert.Null (menuBarItem.TargetView); + + + } +} diff --git a/Tests/UnitTestsParallelizable/Views/MenuItemTests.cs b/Tests/UnitTestsParallelizable/Views/MenuItemTests.cs new file mode 100644 index 0000000000..0e7cf41400 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Views/MenuItemTests.cs @@ -0,0 +1,14 @@ +using Xunit.Abstractions; + +//using static Terminal.Gui.ViewTests.MenuTests; + +namespace Terminal.Gui.ViewsTests; + +public class MenuItemTests () +{ + [Fact] + public void Constructors_Defaults () + { + + } +} diff --git a/Tests/UnitTestsParallelizable/Views/MenuTests.cs b/Tests/UnitTestsParallelizable/Views/MenuTests.cs new file mode 100644 index 0000000000..f29d7273f4 --- /dev/null +++ b/Tests/UnitTestsParallelizable/Views/MenuTests.cs @@ -0,0 +1,17 @@ +using Xunit.Abstractions; + +//using static Terminal.Gui.ViewTests.MenuTests; + +namespace Terminal.Gui.ViewsTests; + +public class MenuTests () +{ + [Fact] + public void Constructors_Defaults () + { + var menu = new Menuv2 { }; + Assert.Empty (menu.Title); + Assert.Empty (menu.Text); + } + +} diff --git a/UICatalog/KeyBindingsDialog.cs b/UICatalog/KeyBindingsDialog.cs deleted file mode 100644 index 73ce9c6af4..0000000000 --- a/UICatalog/KeyBindingsDialog.cs +++ /dev/null @@ -1,211 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using Terminal.Gui; - -namespace UICatalog; - -internal class KeyBindingsDialog : Dialog -{ - // TODO: Update to use Key instead of KeyCode - private static readonly Dictionary CurrentBindings = new (); - - private readonly ObservableCollection _commands; - private readonly ListView _commandsListView; - private readonly Label _keyLabel; - - public KeyBindingsDialog () - { - Title = "Keybindings"; - - //Height = Dim.Percent (80); - //Width = Dim.Percent (80); - if (ViewTracker.Instance == null) - { - ViewTracker.Initialize (); - } - - // known commands that views can support - _commands = new (Enum.GetValues (typeof (Command)).Cast ().ToArray ()); - - _commandsListView = new ListView - { - Width = Dim.Percent (50), - Height = Dim.Fill (Dim.Func (() => IsInitialized ? SubViews.First (view => view.Y.Has (out _)).Frame.Height : 1)), - Source = new ListWrapper (_commands), - SelectedItem = 0 - }; - - Add (_commandsListView); - - _keyLabel = new Label { Text = "Key: None", Width = Dim.Fill (), X = Pos.Percent (50), Y = 0 }; - Add (_keyLabel); - - var btnChange = new Button { X = Pos.Percent (50), Y = 1, Text = "Ch_ange" }; - Add (btnChange); - btnChange.Accepting += RemapKey; - - var close = new Button { Text = "Ok" }; - - close.Accepting += (s, e) => - { - Application.RequestStop (); - ViewTracker.Instance.StartUsingNewKeyMap (CurrentBindings); - }; - AddButton (close); - - var cancel = new Button { Text = "Cancel" }; - cancel.Accepting += (s, e) => Application.RequestStop (); - AddButton (cancel); - - // Register event handler as the last thing in constructor to prevent early calls - // before it is even shown (e.g. OnHasFocusChanging) - _commandsListView.SelectedItemChanged += CommandsListView_SelectedItemChanged; - - // Setup to show first ListView entry - SetTextBoxToShowBinding (_commands.First ()); - } - - private void CommandsListView_SelectedItemChanged (object sender, ListViewItemEventArgs obj) { SetTextBoxToShowBinding ((Command)obj.Value); } - - private void RemapKey (object sender, EventArgs e) - { - Command cmd = _commands [_commandsListView.SelectedItem]; - KeyCode? key = null; - - // prompt user to hit a key - var dlg = new Dialog { Title = "Enter Key" }; - - dlg.KeyDown += (s, k) => - { - key = k.KeyCode; - Application.RequestStop (); - }; - Application.Run (dlg); - dlg.Dispose (); - - if (key.HasValue) - { - CurrentBindings [cmd] = key.Value; - SetTextBoxToShowBinding (cmd); - } - } - - private void SetTextBoxToShowBinding (Command cmd) - { - if (CurrentBindings.ContainsKey (cmd)) - { - _keyLabel.Text = "Key: " + CurrentBindings [cmd]; - } - else - { - _keyLabel.Text = "Key: None"; - } - - SetNeedsDraw (); - } - - /// Tracks views as they are created in UICatalog so that their keybindings can be managed. - private class ViewTracker - { - /// All views seen so far and a bool to indicate if we have applied keybindings to them - private readonly Dictionary _knownViews = new (); - - private readonly object _lockKnownViews = new (); - private Dictionary _keybindings; - - private ViewTracker (View top) - { - RecordView (top); - - // Refresh known windows - Application.AddTimeout ( - TimeSpan.FromMilliseconds (100), - () => - { - lock (_lockKnownViews) - { - RecordView (Application.Top); - - ApplyKeyBindingsToAllKnownViews (); - } - - return true; - } - ); - } - - public static ViewTracker Instance { get; private set; } - internal static void Initialize () { Instance = new ViewTracker (Application.Top); } - - internal void StartUsingNewKeyMap (Dictionary currentBindings) - { - lock (_lockKnownViews) - { - // change our knowledge of what keys to bind - _keybindings = currentBindings; - - // Mark that we have not applied the key bindings yet to any views - foreach (View view in _knownViews.Keys) - { - _knownViews [view] = false; - } - } - } - - private void ApplyKeyBindingsToAllKnownViews () - { - if (_keybindings == null) - { - return; - } - - // Key is the view Value is whether we have already done it - foreach (KeyValuePair viewDone in _knownViews) - { - View view = viewDone.Key; - bool done = viewDone.Value; - - if (done) - { - // we have already applied keybindings to this view - continue; - } - - HashSet supported = new (view.GetSupportedCommands ()); - - foreach (KeyValuePair kvp in _keybindings) - { - // if the view supports the keybinding - if (supported.Contains (kvp.Key)) - { - // if the key was bound to any other commands clear that - view.KeyBindings.Remove (kvp.Value); - view.KeyBindings.Add (kvp.Value, kvp.Key); - } - - // mark that we have done this view so don't need to set keybindings again on it - _knownViews [view] = true; - } - } - } - - private void RecordView (View view) - { - if (!_knownViews.ContainsKey (view)) - { - _knownViews.Add (view, false); - } - - // may already have subviews that were added to it - // before we got to it - foreach (View sub in view.SubViews) - { - RecordView (sub); - } - - view.SubViewAdded += (s, e) => RecordView (e.SubView); - } - } -} diff --git a/UICatalog/Scenarios/CharacterMap/CharacterMap.cs b/UICatalog/Scenarios/CharacterMap/CharacterMap.cs index 028dedfba3..60bdb954f7 100644 --- a/UICatalog/Scenarios/CharacterMap/CharacterMap.cs +++ b/UICatalog/Scenarios/CharacterMap/CharacterMap.cs @@ -146,13 +146,13 @@ public override void Main () top.Add (_categoryList); - var menu = new MenuBar + var menu = new MenuBarv2 { Menus = [ new ( "_File", - new MenuItem [] + new MenuItemv2 [] { new ( "_Quit", @@ -163,7 +163,7 @@ public override void Main () ), new ( "_Options", - new [] { CreateMenuShowWidth () } + new MenuItemv2 [] { CreateMenuShowWidth () } ) ] }; @@ -305,16 +305,19 @@ private EnumerableTableSource CreateCategoryTable (int sortByColum ); } - private MenuItem CreateMenuShowWidth () + private MenuItemv2 CreateMenuShowWidth () { - var item = new MenuItem { Title = "_Show Glyph Width" }; - item.CheckType |= MenuItemCheckStyle.Checked; - item.Checked = _charMap?.ShowGlyphWidths; + CheckBox cb = new () + { + Title = "_Show Glyph Width", + CheckedState = _charMap!.ShowGlyphWidths ? CheckState.Checked : CheckState.None + }; + var item = new MenuItemv2 { CommandView = cb }; item.Action += () => { if (_charMap is { }) { - _charMap.ShowGlyphWidths = (bool)(item.Checked = !item.Checked)!; + _charMap.ShowGlyphWidths = cb.CheckedState == CheckState.Checked; } }; diff --git a/UICatalog/Scenarios/HexEditor.cs b/UICatalog/Scenarios/HexEditor.cs index ca1d694575..9312ff7f84 100644 --- a/UICatalog/Scenarios/HexEditor.cs +++ b/UICatalog/Scenarios/HexEditor.cs @@ -1,5 +1,4 @@ -using System.IO; -using System.Text; +using System.Text; using Terminal.Gui; namespace UICatalog.Scenarios; @@ -14,7 +13,7 @@ public class HexEditor : Scenario { private string _fileName = "demo.bin"; private HexView _hexView; - private MenuItem _miAllowEdits; + private MenuItemv2 _miAllowEdits; private bool _saved = true; private Shortcut _scAddress; private Shortcut _scInfo; @@ -48,13 +47,13 @@ public override void Main () app.Add (_hexView); - var menu = new MenuBar + var menu = new MenuBarv2 { Menus = [ new ( "_File", - new MenuItem [] + new MenuItemv2 [] { new ("_New", "", () => New ()), new ("_Open", "", () => Open ()), @@ -65,7 +64,7 @@ public override void Main () ), new ( "_Edit", - new MenuItem [] + new MenuItemv2 [] { new ("_Copy", "", () => Copy ()), new ("C_ut", "", () => Cut ()), @@ -74,7 +73,7 @@ public override void Main () ), new ( "_Options", - new [] + new MenuItemv2 [] { _miAllowEdits = new ( "_AllowEdits", @@ -82,14 +81,19 @@ public override void Main () () => ToggleAllowEdits () ) { - Checked = _hexView.AllowEdits, - CheckType = MenuItemCheckStyle - .Checked + } } ) ] }; + + CheckBox cb = new CheckBox () + { + Title = _miAllowEdits.Title, + CheckedState = _hexView.AllowEdits ? CheckState.Checked : CheckState.None, + }; + _miAllowEdits.CommandView = cb; app.Add (menu); var addressWidthUpDown = new NumericUpDown @@ -285,5 +289,14 @@ private void Save () } } - private void ToggleAllowEdits () { _hexView.AllowEdits = (bool)(_miAllowEdits.Checked = !_miAllowEdits.Checked); } + private void ToggleAllowEdits () + { + CheckBox? cb = _miAllowEdits.CommandView as CheckBox; + if (cb is null) + { + return; + } + + _hexView.AllowEdits = cb.CheckedState == CheckState.Checked; + } } diff --git a/UICatalog/Scenarios/MenusV2.cs b/UICatalog/Scenarios/MenusV2.cs index 9239f527b6..9a34ea4101 100644 --- a/UICatalog/Scenarios/MenusV2.cs +++ b/UICatalog/Scenarios/MenusV2.cs @@ -439,7 +439,7 @@ private void ConfigureOptionsSubMenu (Menuv2 menu) _menuBgColorCp.ColorChanged += (sender, args) => { - menu.ColorScheme = menu.ColorScheme with + menu.ColorScheme = menu.ColorScheme! with { Normal = new (menu.ColorScheme.Normal.Foreground, args.CurrentValue) }; diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index 4334aabb41..f41f5e3544 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -1,7 +1,4 @@ global using Attribute = Terminal.Gui.Attribute; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; using System.CommandLine; using System.CommandLine.Builder; using System.CommandLine.Parsing; @@ -9,29 +6,25 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.IO; -using System.Linq; using System.Reflection; -using System.Runtime.InteropServices; using System.Text; using System.Text.Json; -using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; -using static Terminal.Gui.ConfigurationManager; -using Command = Terminal.Gui.Command; using Serilog; using Serilog.Core; using Serilog.Events; -using ILogger = Microsoft.Extensions.Logging.ILogger; -using RuntimeEnvironment = Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment; using Terminal.Gui; +using static Terminal.Gui.ConfigurationManager; +using Command = Terminal.Gui.Command; +using ILogger = Microsoft.Extensions.Logging.ILogger; #nullable enable namespace UICatalog; /// -/// UI Catalog is a comprehensive sample library and test app for Terminal.Gui. It provides a simple UI for adding to the +/// UI Catalog is a comprehensive sample library and test app for Terminal.Gui. It provides a simple UI for adding to +/// the /// catalog of scenarios. /// /// @@ -50,85 +43,14 @@ namespace UICatalog; /// /// /// -public class UICatalogApp +public class UICatalog { - private static int _cachedCategoryIndex; - - // When a scenario is run, the main app is killed. These items - // are therefore cached so that when the scenario exits the - // main app UI can be restored to previous state - private static int _cachedScenarioIndex; - private static string? _cachedTheme = string.Empty; - private static ObservableCollection? _categories; - - [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "")] - private static readonly FileSystemWatcher _currentDirWatcher = new (); - - private static ViewDiagnosticFlags _diagnosticFlags; private static string _forceDriver = string.Empty; - [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "")] - private static readonly FileSystemWatcher _homeDirWatcher = new (); - - private static bool _isFirstRunning = true; - private static Options _options; - private static ObservableCollection? _scenarios; - - private const string LOGFILE_LOCATION = "logs"; - private static string _logFilePath = string.Empty; - private static readonly LoggingLevelSwitch _logLevelSwitch = new (); - - // If set, holds the scenario the user selected - private static Scenario? _selectedScenario; - private static MenuBarItem? _themeMenuBarItem; - private static MenuItem []? _themeMenuItems; - private static string _topLevelColorScheme = string.Empty; - - [SerializableConfigurationProperty (Scope = typeof (AppScope), OmitClassName = true)] - [JsonPropertyName ("UICatalog.StatusBar")] - public static bool ShowStatusBar { get; set; } = true; - - /// - /// Gets the message displayed in the About Box. `public` so it can be used from Unit tests. - /// - /// - public static string GetAboutBoxMessage () - { - // NOTE: Do not use multiline verbatim strings here. - // WSL gets all confused. - StringBuilder msg = new (); - msg.AppendLine ("UI Catalog: A comprehensive sample library and test app for"); - msg.AppendLine (); - - msg.AppendLine ( - """ - _______ _ _ _____ _ - |__ __| (_) | | / ____| (_) - | | ___ _ __ _ __ ___ _ _ __ __ _| || | __ _ _ _ - | |/ _ \ '__| '_ ` _ \| | '_ \ / _` | || | |_ | | | | | - | | __/ | | | | | | | | | | | (_| | || |__| | |_| | | - |_|\___|_| |_| |_| |_|_|_| |_|\__,_|_(_)_____|\__,_|_| - """); - msg.AppendLine (); - msg.AppendLine ("v2 - Pre-Alpha"); - msg.AppendLine (); - msg.AppendLine ("https://github.com/gui-cs/Terminal.Gui"); - - return msg.ToString (); - } - - private static void ConfigFileChanged (object sender, FileSystemEventArgs e) - { - if (Application.Top == null) - { - return; - } - - // TODO: This is a hack. Figure out how to ensure that the file is fully written before reading it. - //Thread.Sleep (500); - Load (); - Apply (); - } + public static string LogFilePath { get; set; } = string.Empty; + public static LoggingLevelSwitch LogLevelSwitch { get; } = new (); + public const string LOGFILE_LOCATION = "logs"; + public static UICatalogCommandLineOptions Options { get; set; } private static int Main (string [] args) { @@ -139,18 +61,18 @@ private static int Main (string [] args) CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US"); } - _scenarios = Scenario.GetScenarios (); - _categories = Scenario.GetAllCategories (); + UICatalogTopLevel.CachedScenarios = Scenario.GetScenarios (); + UICatalogTopLevel.CachedCategories = Scenario.GetAllCategories (); // Process command line args // If no driver is provided, the default driver is used. Option driverOption = new Option ("--driver", "The IConsoleDriver to use.").FromAmong ( Application.GetDriverTypes () - .Where (d=>!typeof (IConsoleDriverFacade).IsAssignableFrom (d)) - .Select (d => d!.Name) - .Union (["v2","v2win","v2net"]) - .ToArray () + .Where (d => !typeof (IConsoleDriverFacade).IsAssignableFrom (d)) + .Select (d => d!.Name) + .Union (["v2", "v2win", "v2net"]) + .ToArray () ); driverOption.AddAlias ("-d"); driverOption.AddAlias ("--d"); @@ -171,11 +93,12 @@ private static int Main (string [] args) resultsFile.AddAlias ("--f"); // what's the app name? - _logFilePath = $"{LOGFILE_LOCATION}/{Assembly.GetExecutingAssembly ().GetName ().Name}"; - Option debugLogLevel = new Option ("--debug-log-level", $"The level to use for logging (debug console and {_logFilePath})").FromAmong ( + LogFilePath = $"{LOGFILE_LOCATION}/{Assembly.GetExecutingAssembly ().GetName ().Name}"; + + Option debugLogLevel = new Option ("--debug-log-level", $"The level to use for logging (debug console and {LogFilePath})").FromAmong ( Enum.GetNames () ); - debugLogLevel.SetDefaultValue("Warning"); + debugLogLevel.SetDefaultValue ("Warning"); debugLogLevel.AddAlias ("-dl"); debugLogLevel.AddAlias ("--dl"); @@ -185,9 +108,9 @@ private static int Main (string [] args) "The name of the Scenario to run. If not provided, the UI Catalog UI will be shown.", getDefaultValue: () => "none" ).FromAmong ( - _scenarios.Select (s => s.GetName ()) - .Append ("none") - .ToArray () + UICatalogTopLevel.CachedScenarios.Select (s => s.GetName ()) + .Append ("none") + .ToArray () ); var rootCommand = new RootCommand ("A comprehensive sample library and test app for Terminal.Gui") @@ -198,7 +121,7 @@ private static int Main (string [] args) rootCommand.SetHandler ( context => { - var options = new Options + var options = new UICatalogCommandLineOptions { Scenario = context.ParseResult.GetValueForArgument (scenarioArgument), Driver = context.ParseResult.GetValueForOption (driverOption) ?? string.Empty, @@ -210,7 +133,7 @@ private static int Main (string [] args) }; // See https://github.com/dotnet/command-line-api/issues/796 for the rationale behind this hackery - _options = options; + Options = options; } ); @@ -227,16 +150,16 @@ private static int Main (string [] args) return 0; } - Scenario.BenchmarkTimeout = _options.BenchmarkTimeout; + Scenario.BenchmarkTimeout = Options.BenchmarkTimeout; Logging.Logger = CreateLogger (); - UICatalogMain (_options); + UICatalogMain (Options); return 0; } - private static LogEventLevel LogLevelToLogEventLevel (LogLevel logLevel) + public static LogEventLevel LogLevelToLogEventLevel (LogLevel logLevel) { return logLevel switch { @@ -254,13 +177,14 @@ private static LogEventLevel LogLevelToLogEventLevel (LogLevel logLevel) private static ILogger CreateLogger () { // Configure Serilog to write logs to a file - _logLevelSwitch.MinimumLevel = LogLevelToLogEventLevel(Enum.Parse (_options.DebugLogLevel)); + LogLevelSwitch.MinimumLevel = LogLevelToLogEventLevel (Enum.Parse (Options.DebugLogLevel)); + Log.Logger = new LoggerConfiguration () - .MinimumLevel.ControlledBy (_logLevelSwitch) + .MinimumLevel.ControlledBy (LogLevelSwitch) .Enrich.FromLogContext () // Enables dynamic enrichment .WriteTo.Debug () .WriteTo.File ( - _logFilePath, + LogFilePath, rollingInterval: RollingInterval.Day, outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}") .CreateLogger (); @@ -278,35 +202,6 @@ private static ILogger CreateLogger () return loggerFactory.CreateLogger ("Global Logger"); } - public static void OpenUrl (string url) - { - if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) - { - url = url.Replace ("&", "^&"); - Process.Start (new ProcessStartInfo ("cmd", $"/c start {url}") { CreateNoWindow = true }); - } - else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) - { - using var process = new Process - { - StartInfo = new () - { - FileName = "xdg-open", - Arguments = url, - RedirectStandardError = true, - RedirectStandardOutput = true, - CreateNoWindow = true, - UseShellExecute = false - } - }; - process.Start (); - } - else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) - { - Process.Start ("open", url); - } - } - /// /// Shows the UI Catalog selection UI. When the user selects a Scenario to run, the UI Catalog main app UI is /// killed and the Scenario is run as though it were Application.Top. When the Scenario exits, this function exits. @@ -322,25 +217,31 @@ private static Scenario RunUICatalogTopLevel () Application.Init (driverName: _forceDriver); - if (_cachedTheme is null) + if (string.IsNullOrWhiteSpace (UICatalogTopLevel.CachedTheme)) { - _cachedTheme = Themes?.Theme; + UICatalogTopLevel.CachedTheme = Themes?.Theme; } else { - Themes!.Theme = _cachedTheme; + Themes!.Theme = UICatalogTopLevel.CachedTheme; Apply (); } Application.Run ().Dispose (); Application.Shutdown (); - return _selectedScenario!; + return UICatalogTopLevel.CachedSelectedScenario!; } + [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "")] + private static readonly FileSystemWatcher _currentDirWatcher = new (); + + [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "")] + private static readonly FileSystemWatcher _homeDirWatcher = new (); + private static void StartConfigFileWatcher () { - // Setup a file system watcher for `./.tui/` + // Set up a file system watcher for `./.tui/` _currentDirWatcher.NotifyFilter = NotifyFilters.LastWrite; string assemblyLocation = Assembly.GetExecutingAssembly ().Location; @@ -364,7 +265,7 @@ private static void StartConfigFileWatcher () _currentDirWatcher.Path = tuiDir; _currentDirWatcher.Filter = "*config.json"; - // Setup a file system watcher for `~/.tui/` + // Set up a file system watcher for `~/.tui/` _homeDirWatcher.NotifyFilter = NotifyFilters.LastWrite; var f = new FileInfo (Environment.GetFolderPath (Environment.SpecialFolder.UserProfile)); tuiDir = Path.Combine (f.FullName, ".tui"); @@ -399,7 +300,18 @@ private static void StopConfigFileWatcher () _homeDirWatcher.Created -= ConfigFileChanged; } - private static void UICatalogMain (Options options) + private static void ConfigFileChanged (object sender, FileSystemEventArgs e) + { + if (Application.Top == null) + { + return; + } + + Load (); + Apply (); + } + + private static void UICatalogMain (UICatalogCommandLineOptions options) { StartConfigFileWatcher (); @@ -411,17 +323,15 @@ private static void UICatalogMain (Options options) // run it and exit when done. if (options.Scenario != "none") { - _topLevelColorScheme = "Base"; - - int item = _scenarios!.IndexOf ( - _scenarios!.FirstOrDefault ( + int item = UICatalogTopLevel.CachedScenarios!.IndexOf ( + UICatalogTopLevel.CachedScenarios!.FirstOrDefault ( s => s.GetName () .Equals (options.Scenario, StringComparison.OrdinalIgnoreCase) )!); - _selectedScenario = (Scenario)Activator.CreateInstance (_scenarios [item].GetType ())!; + UICatalogTopLevel.CachedSelectedScenario = (Scenario)Activator.CreateInstance (UICatalogTopLevel.CachedScenarios [item].GetType ())!; - BenchmarkResults? results = RunScenario (_selectedScenario, options.Benchmark); + BenchmarkResults? results = RunScenario (UICatalogTopLevel.CachedSelectedScenario, options.Benchmark); if (results is { }) { @@ -450,12 +360,13 @@ private static void UICatalogMain (Options options) while (RunUICatalogTopLevel () is { } scenario) { VerifyObjectsWereDisposed (); - Themes!.Theme = _cachedTheme!; + Themes!.Theme = UICatalogTopLevel.CachedTheme!; Apply (); - scenario.TopLevelColorScheme = _topLevelColorScheme; + scenario.TopLevelColorScheme = UICatalogTopLevel.CachedTopLevelColorScheme!; #if DEBUG_IDISPOSABLE View.DebugIDisposable = true; + // Measure how long it takes for the app to shut down var sw = new Stopwatch (); string scenarioName = scenario.GetName (); @@ -501,7 +412,7 @@ void ApplicationOnInitializedChanged (object? sender, EventArgs e) } Application.Init (driverName: _forceDriver); - scenario.TopLevelColorScheme = _topLevelColorScheme; + scenario.TopLevelColorScheme = UICatalogTopLevel.CachedTopLevelColorScheme!; if (benchmark) { @@ -527,11 +438,11 @@ void ApplicationOnInitializedChanged (object? sender, EventArgs e) private static void BenchmarkAllScenarios () { - List resultsList = new (); + List resultsList = []; var maxScenarios = 5; - foreach (Scenario s in _scenarios!) + foreach (Scenario s in UICatalogTopLevel.CachedScenarios!) { resultsList.Add (RunScenario (s, true)!); maxScenarios--; @@ -542,144 +453,146 @@ private static void BenchmarkAllScenarios () } } - if (resultsList.Count > 0) + if (resultsList.Count <= 0) { - if (!string.IsNullOrEmpty (_options.ResultsFile)) - { - string output = JsonSerializer.Serialize ( - resultsList, - new JsonSerializerOptions - { - WriteIndented = true - }); - - using StreamWriter file = File.CreateText (_options.ResultsFile); - file.Write (output); - file.Close (); - - return; - } + return; + } - Application.Init (); + if (!string.IsNullOrEmpty (Options.ResultsFile)) + { + string output = JsonSerializer.Serialize ( + resultsList, + new JsonSerializerOptions + { + WriteIndented = true + }); - var benchmarkWindow = new Window - { - Title = "Benchmark Results" - }; + using StreamWriter file = File.CreateText (Options.ResultsFile); + file.Write (output); + file.Close (); - if (benchmarkWindow.Border is { }) - { - benchmarkWindow.Border.Thickness = new (0, 0, 0, 0); - } + return; + } - TableView resultsTableView = new () - { - Width = Dim.Fill (), - Height = Dim.Fill () - }; - - // TableView provides many options for table headers. For simplicity we turn all - // of these off. By enabling FullRowSelect and turning off headers, TableView looks just - // like a ListView - resultsTableView.FullRowSelect = true; - resultsTableView.Style.ShowHeaders = true; - resultsTableView.Style.ShowHorizontalHeaderOverline = false; - resultsTableView.Style.ShowHorizontalHeaderUnderline = true; - resultsTableView.Style.ShowHorizontalBottomline = false; - resultsTableView.Style.ShowVerticalCellLines = true; - resultsTableView.Style.ShowVerticalHeaderLines = true; - - /* By default TableView lays out columns at render time and only - * measures y rows of data at a time. Where y is the height of the - * console. This is for the following reasons: - * - * - Performance, when tables have a large amount of data - * - Defensive, prevents a single wide cell value pushing other - * columns off screen (requiring horizontal scrolling - * - * In the case of UICatalog here, such an approach is overkill so - * we just measure all the data ourselves and set the appropriate - * max widths as ColumnStyles - */ - //int longestName = _scenarios!.Max (s => s.GetName ().Length); - - //resultsTableView.Style.ColumnStyles.Add ( - // 0, - // new () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName } - // ); - //resultsTableView.Style.ColumnStyles.Add (1, new () { MaxWidth = 1 }); - //resultsTableView.CellActivated += ScenarioView_OpenSelectedItem; - - // TableView typically is a grid where nav keys are biased for moving left/right. - resultsTableView.KeyBindings.Remove (Key.Home); - resultsTableView.KeyBindings.Add (Key.Home, Command.Start); - resultsTableView.KeyBindings.Remove (Key.End); - resultsTableView.KeyBindings.Add (Key.End, Command.End); - - // Ideally, TableView.MultiSelect = false would turn off any keybindings for - // multi-select options. But it currently does not. UI Catalog uses Ctrl-A for - // a shortcut to About. - resultsTableView.MultiSelect = false; - - var dt = new DataTable (); - - dt.Columns.Add (new DataColumn ("Scenario", typeof (string))); - dt.Columns.Add (new DataColumn ("Duration", typeof (TimeSpan))); - dt.Columns.Add (new DataColumn ("Refreshed", typeof (int))); - dt.Columns.Add (new DataColumn ("LaidOut", typeof (int))); - dt.Columns.Add (new DataColumn ("ClearedContent", typeof (int))); - dt.Columns.Add (new DataColumn ("DrawComplete", typeof (int))); - dt.Columns.Add (new DataColumn ("Updated", typeof (int))); - dt.Columns.Add (new DataColumn ("Iterations", typeof (int))); - - foreach (BenchmarkResults r in resultsList) - { - dt.Rows.Add ( - r.Scenario, - r.Duration, - r.RefreshedCount, - r.LaidOutCount, - r.ClearedContentCount, - r.DrawCompleteCount, - r.UpdatedCount, - r.IterationCount - ); - } + Application.Init (); - BenchmarkResults totalRow = new () - { - Scenario = "TOTAL", - Duration = new (resultsList.Sum (r => r.Duration.Ticks)), - RefreshedCount = resultsList.Sum (r => r.RefreshedCount), - LaidOutCount = resultsList.Sum (r => r.LaidOutCount), - ClearedContentCount = resultsList.Sum (r => r.ClearedContentCount), - DrawCompleteCount = resultsList.Sum (r => r.DrawCompleteCount), - UpdatedCount = resultsList.Sum (r => r.UpdatedCount), - IterationCount = resultsList.Sum (r => r.IterationCount) - }; + var benchmarkWindow = new Window + { + Title = "Benchmark Results" + }; + + if (benchmarkWindow.Border is { }) + { + benchmarkWindow.Border.Thickness = new (0, 0, 0, 0); + } + + TableView resultsTableView = new () + { + Width = Dim.Fill (), + Height = Dim.Fill () + }; + // TableView provides many options for table headers. For simplicity we turn all + // of these off. By enabling FullRowSelect and turning off headers, TableView looks just + // like a ListView + resultsTableView.FullRowSelect = true; + resultsTableView.Style.ShowHeaders = true; + resultsTableView.Style.ShowHorizontalHeaderOverline = false; + resultsTableView.Style.ShowHorizontalHeaderUnderline = true; + resultsTableView.Style.ShowHorizontalBottomline = false; + resultsTableView.Style.ShowVerticalCellLines = true; + resultsTableView.Style.ShowVerticalHeaderLines = true; + + /* By default, TableView lays out columns at render time and only + * measures y rows of data at a time. Where y is the height of the + * console. This is for the following reasons: + * + * - Performance, when tables have a large amount of data + * - Defensive, prevents a single wide cell value pushing other + * columns off-screen (requiring horizontal scrolling + * + * In the case of UICatalog here, such an approach is overkill so + * we just measure all the data ourselves and set the appropriate + * max widths as ColumnStyles + */ + //int longestName = _scenarios!.Max (s => s.GetName ().Length); + + //resultsTableView.Style.ColumnStyles.Add ( + // 0, + // new () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName } + // ); + //resultsTableView.Style.ColumnStyles.Add (1, new () { MaxWidth = 1 }); + //resultsTableView.CellActivated += ScenarioView_OpenSelectedItem; + + // TableView typically is a grid where nav keys are biased for moving left/right. + resultsTableView.KeyBindings.Remove (Key.Home); + resultsTableView.KeyBindings.Add (Key.Home, Command.Start); + resultsTableView.KeyBindings.Remove (Key.End); + resultsTableView.KeyBindings.Add (Key.End, Command.End); + + // Ideally, TableView.MultiSelect = false would turn off any keybindings for + // multi-select options. But it currently does not. UI Catalog uses Ctrl-A for + // a shortcut to About. + resultsTableView.MultiSelect = false; + + var dt = new DataTable (); + + dt.Columns.Add (new DataColumn ("Scenario", typeof (string))); + dt.Columns.Add (new DataColumn ("Duration", typeof (TimeSpan))); + dt.Columns.Add (new DataColumn ("Refreshed", typeof (int))); + dt.Columns.Add (new DataColumn ("LaidOut", typeof (int))); + dt.Columns.Add (new DataColumn ("ClearedContent", typeof (int))); + dt.Columns.Add (new DataColumn ("DrawComplete", typeof (int))); + dt.Columns.Add (new DataColumn ("Updated", typeof (int))); + dt.Columns.Add (new DataColumn ("Iterations", typeof (int))); + + foreach (BenchmarkResults r in resultsList) + { dt.Rows.Add ( - totalRow.Scenario, - totalRow.Duration, - totalRow.RefreshedCount, - totalRow.LaidOutCount, - totalRow.ClearedContentCount, - totalRow.DrawCompleteCount, - totalRow.UpdatedCount, - totalRow.IterationCount + r.Scenario, + r.Duration, + r.RefreshedCount, + r.LaidOutCount, + r.ClearedContentCount, + r.DrawCompleteCount, + r.UpdatedCount, + r.IterationCount ); + } - dt.DefaultView.Sort = "Duration"; - DataTable sortedCopy = dt.DefaultView.ToTable (); + BenchmarkResults totalRow = new () + { + Scenario = "TOTAL", + Duration = new (resultsList.Sum (r => r.Duration.Ticks)), + RefreshedCount = resultsList.Sum (r => r.RefreshedCount), + LaidOutCount = resultsList.Sum (r => r.LaidOutCount), + ClearedContentCount = resultsList.Sum (r => r.ClearedContentCount), + DrawCompleteCount = resultsList.Sum (r => r.DrawCompleteCount), + UpdatedCount = resultsList.Sum (r => r.UpdatedCount), + IterationCount = resultsList.Sum (r => r.IterationCount) + }; - resultsTableView.Table = new DataTableSource (sortedCopy); + dt.Rows.Add ( + totalRow.Scenario, + totalRow.Duration, + totalRow.RefreshedCount, + totalRow.LaidOutCount, + totalRow.ClearedContentCount, + totalRow.DrawCompleteCount, + totalRow.UpdatedCount, + totalRow.IterationCount + ); - benchmarkWindow.Add (resultsTableView); + dt.DefaultView.Sort = "Duration"; + DataTable sortedCopy = dt.DefaultView.ToTable (); - Application.Run (benchmarkWindow); - benchmarkWindow.Dispose (); - Application.Shutdown (); - } + resultsTableView.Table = new DataTableSource (sortedCopy); + + benchmarkWindow.Add (resultsTableView); + + Application.Run (benchmarkWindow); + benchmarkWindow.Dispose (); + Application.Shutdown (); } private static void VerifyObjectsWereDisposed () @@ -711,848 +624,4 @@ private static void VerifyObjectsWereDisposed () RunState.Instances.Clear (); #endif } - - /// - /// This is the main UI Catalog app view. It is run fresh when the app loads (if a Scenario has not been passed on - /// the command line) and each time a Scenario ends. - /// - public class UICatalogTopLevel : Toplevel - { - public ListView? CategoryList; - public MenuItem? MiForce16Colors; - public MenuItem? MiIsMenuBorderDisabled; - public MenuItem? MiIsMouseDisabled; - public MenuItem? MiUseSubMenusSingleFrame; - - public Shortcut? ShForce16Colors; - - //public Shortcut? ShDiagnostics; - public Shortcut? ShVersion; - - // UI Catalog uses TableView for the scenario list instead of a ListView to demonstate how - // TableView works. There's no real reason not to use ListView. Because we use TableView, and TableView - // doesn't (currently) have CollectionNavigator support built in, we implement it here, within the app. - public TableView ScenarioList; - - private readonly StatusBar? _statusBar; - - private readonly CollectionNavigator _scenarioCollectionNav = new (); - - public UICatalogTopLevel () - { - _diagnosticFlags = Diagnostics; - - _themeMenuItems = CreateThemeMenuItems (); - _themeMenuBarItem = new ("_Themes", _themeMenuItems!); - - MenuBar menuBar = new () - { - Menus = - [ - new ( - "_File", - new MenuItem [] - { - new ( - "_Quit", - "Quit UI Catalog", - RequestStop - ) - } - ), - _themeMenuBarItem, - new ("Diag_nostics", CreateDiagnosticMenuItems ()), - new ("_Logging", CreateLoggingMenuItems ()), - new ( - "_Help", - new MenuItem [] - { - new ( - "_Documentation", - "", - () => OpenUrl ("https://gui-cs.github.io/Terminal.GuiV2Docs"), - null, - null, - (KeyCode)Key.F1 - ), - new ( - "_README", - "", - () => OpenUrl ("https://github.com/gui-cs/Terminal.Gui"), - null, - null, - (KeyCode)Key.F2 - ), - new ( - "_About...", - "About UI Catalog", - () => MessageBox.Query ( - "", - GetAboutBoxMessage (), - wrapMessage: false, - buttons: "_Ok" - ), - null, - null, - (KeyCode)Key.A.WithCtrl - ) - } - ) - ] - }; - - _statusBar = new () - { - Visible = ShowStatusBar, - AlignmentModes = AlignmentModes.IgnoreFirstOrLast, - CanFocus = false - }; - _statusBar.Height = Dim.Auto (DimAutoStyle.Auto, minimumContentDim: Dim.Func (() => _statusBar.Visible ? 1 : 0), maximumContentDim: Dim.Func (() => _statusBar.Visible ? 1 : 0)); - - ShVersion = new () - { - Title = "Version Info", - CanFocus = false - }; - - var statusBarShortcut = new Shortcut - { - Key = Key.F10, - Title = "Show/Hide Status Bar", - CanFocus = false - }; - - statusBarShortcut.Accepting += (sender, args) => - { - _statusBar.Visible = !_statusBar.Visible; - args.Cancel = true; - }; - - ShForce16Colors = new () - { - CanFocus = false, - CommandView = new CheckBox - { - Title = "16 color mode", - CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked, - CanFocus = false - }, - HelpText = "", - BindKeyToApplication = true, - Key = Key.F7 - }; - - ((CheckBox)ShForce16Colors.CommandView).CheckedStateChanging += (sender, args) => - { - Application.Force16Colors = args.NewValue == CheckState.Checked; - MiForce16Colors!.Checked = Application.Force16Colors; - Application.LayoutAndDraw (); - }; - - _statusBar.Add ( - new Shortcut - { - CanFocus = false, - Title = "Quit", - Key = Application.QuitKey - }, - statusBarShortcut, - ShForce16Colors, - - //ShDiagnostics, - ShVersion - ); - - // Create the Category list view. This list never changes. - CategoryList = new () - { - X = 0, - Y = Pos.Bottom (menuBar), - Width = Dim.Auto (), - Height = Dim.Fill ( - Dim.Func ( - () => - { - if (_statusBar.NeedsLayout) - { - throw new LayoutException ("DimFunc.Fn aborted because dependent View needs layout."); - - //_statusBar.Layout (); - } - - return _statusBar.Frame.Height; - })), - AllowsMarking = false, - CanFocus = true, - Title = "_Categories", - BorderStyle = LineStyle.Rounded, - SuperViewRendersLineCanvas = true, - Source = new ListWrapper (_categories) - }; - CategoryList.OpenSelectedItem += (s, a) => { ScenarioList!.SetFocus (); }; - CategoryList.SelectedItemChanged += CategoryView_SelectedChanged; - - // This enables the scrollbar by causing lazy instantiation to happen - CategoryList.VerticalScrollBar.AutoShow = true; - - // Create the scenario list. The contents of the scenario list changes whenever the - // Category list selection changes (to show just the scenarios that belong to the selected - // category). - ScenarioList = new () - { - X = Pos.Right (CategoryList) - 1, - Y = Pos.Bottom (menuBar), - Width = Dim.Fill (), - Height = Dim.Fill ( - Dim.Func ( - () => - { - if (_statusBar.NeedsLayout) - { - throw new LayoutException ("DimFunc.Fn aborted because dependent View needs layout."); - - //_statusBar.Layout (); - } - - return _statusBar.Frame.Height; - })), - - //AllowsMarking = false, - CanFocus = true, - Title = "_Scenarios", - BorderStyle = CategoryList.BorderStyle, - SuperViewRendersLineCanvas = true - }; - - //ScenarioList.VerticalScrollBar.AutoHide = false; - //ScenarioList.HorizontalScrollBar.AutoHide = false; - - // TableView provides many options for table headers. For simplicity we turn all - // of these off. By enabling FullRowSelect and turning off headers, TableView looks just - // like a ListView - ScenarioList.FullRowSelect = true; - ScenarioList.Style.ShowHeaders = false; - ScenarioList.Style.ShowHorizontalHeaderOverline = false; - ScenarioList.Style.ShowHorizontalHeaderUnderline = false; - ScenarioList.Style.ShowHorizontalBottomline = false; - ScenarioList.Style.ShowVerticalCellLines = false; - ScenarioList.Style.ShowVerticalHeaderLines = false; - - /* By default TableView lays out columns at render time and only - * measures y rows of data at a time. Where y is the height of the - * console. This is for the following reasons: - * - * - Performance, when tables have a large amount of data - * - Defensive, prevents a single wide cell value pushing other - * columns off screen (requiring horizontal scrolling - * - * In the case of UICatalog here, such an approach is overkill so - * we just measure all the data ourselves and set the appropriate - * max widths as ColumnStyles - */ - int longestName = _scenarios!.Max (s => s.GetName ().Length); - - ScenarioList.Style.ColumnStyles.Add ( - 0, - new () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName } - ); - ScenarioList.Style.ColumnStyles.Add (1, new () { MaxWidth = 1 }); - ScenarioList.CellActivated += ScenarioView_OpenSelectedItem; - - // TableView typically is a grid where nav keys are biased for moving left/right. - ScenarioList.KeyBindings.Remove (Key.Home); - ScenarioList.KeyBindings.Add (Key.Home, Command.Start); - ScenarioList.KeyBindings.Remove (Key.End); - ScenarioList.KeyBindings.Add (Key.End, Command.End); - - // Ideally, TableView.MultiSelect = false would turn off any keybindings for - // multi-select options. But it currently does not. UI Catalog uses Ctrl-A for - // a shortcut to About. - ScenarioList.MultiSelect = false; - ScenarioList.KeyBindings.Remove (Key.A.WithCtrl); - - Add (menuBar); - Add (CategoryList); - Add (ScenarioList); - Add (_statusBar); - - Loaded += LoadedHandler; - Unloaded += UnloadedHandler; - - // Restore previous selections - CategoryList.SelectedItem = _cachedCategoryIndex; - ScenarioList.SelectedRow = _cachedScenarioIndex; - - Applied += ConfigAppliedHandler; - } - - public void ConfigChanged () - { - if (MenuBar == null) - { - // View is probably disposed - return; - } - - if (_topLevelColorScheme == null || !Colors.ColorSchemes.ContainsKey (_topLevelColorScheme)) - { - _topLevelColorScheme = "Base"; - } - - _cachedTheme = Themes?.Theme; - - _themeMenuItems = CreateThemeMenuItems (); - _themeMenuBarItem!.Children = _themeMenuItems; - - foreach (MenuItem mi in _themeMenuItems!) - { - if (mi is { Parent: null }) - { - mi.Parent = _themeMenuBarItem; - } - } - - ColorScheme = Colors.ColorSchemes [_topLevelColorScheme]; - - MenuBar!.Menus [0].Children! [0]!.ShortcutKey = Application.QuitKey; - - ((Shortcut)_statusBar!.SubViews.ElementAt (0)).Key = Application.QuitKey; - _statusBar.Visible = ShowStatusBar; - - MiIsMouseDisabled!.Checked = Application.IsMouseDisabled; - - ((CheckBox)ShForce16Colors!.CommandView!).CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked; - - Application.Top!.SetNeedsDraw (); - } - - public MenuItem []? CreateThemeMenuItems () - { - List menuItems = CreateForce16ColorItems ().ToList (); - menuItems.Add (null!); - - var schemeCount = 0; - - foreach (KeyValuePair theme in Themes!) - { - var item = new MenuItem - { - Title = theme.Key == "Dark" ? $"{theme.Key.Substring (0, 3)}_{theme.Key.Substring (3, 1)}" : $"_{theme.Key}", - ShortcutKey = new Key ((KeyCode)((uint)KeyCode.D1 + schemeCount++)) - .WithCtrl - }; - item.CheckType |= MenuItemCheckStyle.Checked; - item.Checked = theme.Key == _cachedTheme; // CM.Themes.Theme; - - item.Action += () => - { - Themes.Theme = _cachedTheme = theme.Key; - Apply (); - }; - menuItems.Add (item); - } - - List schemeMenuItems = new (); - - foreach (KeyValuePair sc in Colors.ColorSchemes) - { - var item = new MenuItem { Title = $"_{sc.Key}", Data = sc.Key }; - item.CheckType |= MenuItemCheckStyle.Radio; - item.Checked = sc.Key == _topLevelColorScheme; - - item.Action += () => - { - _topLevelColorScheme = (string)item.Data; - - foreach (MenuItem schemeMenuItem in schemeMenuItems) - { - schemeMenuItem.Checked = (string)schemeMenuItem.Data == _topLevelColorScheme; - } - - ColorScheme = Colors.ColorSchemes [_topLevelColorScheme]; - }; - item.ShortcutKey = ((Key)sc.Key [0].ToString ().ToLower ()).WithCtrl; - schemeMenuItems.Add (item); - } - - menuItems.Add (null!); - var mbi = new MenuBarItem ("_Color Scheme for Application.Top", schemeMenuItems.ToArray ()); - menuItems.Add (mbi); - - return menuItems.ToArray (); - } - - private void CategoryView_SelectedChanged (object? sender, ListViewItemEventArgs? e) - { - string item = _categories! [e!.Item]; - ObservableCollection newlist; - - if (e.Item == 0) - { - // First category is "All" - newlist = _scenarios!; - } - else - { - newlist = new (_scenarios!.Where (s => s.GetCategories ().Contains (item)).ToList ()); - } - - ScenarioList.Table = new EnumerableTableSource ( - newlist, - new () - { - { "Name", s => s.GetName () }, { "Description", s => s.GetDescription () } - } - ); - - // Create a collection of just the scenario names (the 1st column in our TableView) - // for CollectionNavigator. - List firstColumnList = new (); - - for (var i = 0; i < ScenarioList.Table.Rows; i++) - { - firstColumnList.Add (ScenarioList.Table [i, 0]); - } - - _scenarioCollectionNav.Collection = firstColumnList; - } - - private void ConfigAppliedHandler (object? sender, ConfigurationManagerEventArgs? a) { ConfigChanged (); } - - [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "")] - private MenuItem [] CreateDiagnosticFlagsMenuItems () - { - const string OFF = "View Diagnostics: _Off"; - const string RULER = "View Diagnostics: _Ruler"; - const string THICKNESS = "View Diagnostics: _Thickness"; - const string HOVER = "View Diagnostics: _Hover"; - const string DRAWINDICATOR = "View Diagnostics: _DrawIndicator"; - var index = 0; - - List menuItems = new (); - - foreach (Enum diag in Enum.GetValues (_diagnosticFlags.GetType ())) - { - var item = new MenuItem - { - Title = GetDiagnosticsTitle (diag), ShortcutKey = new Key (index.ToString () [0]).WithAlt - }; - index++; - item.CheckType |= MenuItemCheckStyle.Checked; - - if (GetDiagnosticsTitle (ViewDiagnosticFlags.Off) == item.Title) - { - item.Checked = !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.Thickness) - && !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.Ruler) - && !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.Hover) - && !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.DrawIndicator); - } - else - { - item.Checked = _diagnosticFlags.HasFlag (diag); - } - - item.Action += () => - { - string t = GetDiagnosticsTitle (ViewDiagnosticFlags.Off); - - if (item.Title == t && item.Checked == false) - { - _diagnosticFlags &= ~(ViewDiagnosticFlags.Thickness - | ViewDiagnosticFlags.Ruler - | ViewDiagnosticFlags.Hover - | ViewDiagnosticFlags.DrawIndicator); - item.Checked = true; - } - else if (item.Title == t && item.Checked == true) - { - _diagnosticFlags |= ViewDiagnosticFlags.Thickness - | ViewDiagnosticFlags.Ruler - | ViewDiagnosticFlags.Hover - | ViewDiagnosticFlags.DrawIndicator; - item.Checked = false; - } - else - { - Enum f = GetDiagnosticsEnumValue (item.Title); - - if (_diagnosticFlags.HasFlag (f)) - { - SetDiagnosticsFlag (f, false); - } - else - { - SetDiagnosticsFlag (f, true); - } - } - - foreach (MenuItem menuItem in menuItems) - { - if (menuItem.Title == t) - { - menuItem.Checked = !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.Ruler) - && !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.Thickness) - && !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.Hover) - && !_diagnosticFlags.HasFlag (ViewDiagnosticFlags.DrawIndicator); - } - else if (menuItem.Title != t) - { - menuItem.Checked = _diagnosticFlags.HasFlag (GetDiagnosticsEnumValue (menuItem.Title)); - } - } - - Diagnostics = _diagnosticFlags; - }; - menuItems.Add (item); - } - - return menuItems.ToArray (); - - string GetDiagnosticsTitle (Enum diag) - { - return Enum.GetName (_diagnosticFlags.GetType (), diag) switch - { - "Off" => OFF, - "Ruler" => RULER, - "Thickness" => THICKNESS, - "Hover" => HOVER, - "DrawIndicator" => DRAWINDICATOR, - _ => "" - }; - } - - Enum GetDiagnosticsEnumValue (string? title) - { - return title switch - { - RULER => ViewDiagnosticFlags.Ruler, - THICKNESS => ViewDiagnosticFlags.Thickness, - HOVER => ViewDiagnosticFlags.Hover, - DRAWINDICATOR => ViewDiagnosticFlags.DrawIndicator, - _ => null! - }; - } - - void SetDiagnosticsFlag (Enum diag, bool add) - { - switch (diag) - { - case ViewDiagnosticFlags.Ruler: - if (add) - { - _diagnosticFlags |= ViewDiagnosticFlags.Ruler; - } - else - { - _diagnosticFlags &= ~ViewDiagnosticFlags.Ruler; - } - - break; - case ViewDiagnosticFlags.Thickness: - if (add) - { - _diagnosticFlags |= ViewDiagnosticFlags.Thickness; - } - else - { - _diagnosticFlags &= ~ViewDiagnosticFlags.Thickness; - } - - break; - case ViewDiagnosticFlags.Hover: - if (add) - { - _diagnosticFlags |= ViewDiagnosticFlags.Hover; - } - else - { - _diagnosticFlags &= ~ViewDiagnosticFlags.Hover; - } - - break; - case ViewDiagnosticFlags.DrawIndicator: - if (add) - { - _diagnosticFlags |= ViewDiagnosticFlags.DrawIndicator; - } - else - { - _diagnosticFlags &= ~ViewDiagnosticFlags.DrawIndicator; - } - - break; - default: - _diagnosticFlags = default (ViewDiagnosticFlags); - - break; - } - } - } - - private List CreateDiagnosticMenuItems () - { - List menuItems = new () - { - CreateDiagnosticFlagsMenuItems (), - new MenuItem [] { null! }, - CreateDisabledEnabledMouseItems (), - CreateDisabledEnabledMenuBorder (), - CreateDisabledEnableUseSubMenusSingleFrame (), - CreateKeyBindingsMenuItems () - }; - - return menuItems; - } - - private List CreateLoggingMenuItems () - { - List menuItems = new () - { - CreateLoggingFlagsMenuItems ()! - }; - - return menuItems; - } - - [SuppressMessage ("Style", "IDE1006:Naming Styles", Justification = "")] - private MenuItem? [] CreateLoggingFlagsMenuItems () - { - string [] logLevelMenuStrings = Enum.GetNames ().Select (n => n = "_" + n).ToArray (); - LogLevel [] logLevels = Enum.GetValues (); - - List menuItems = new (); - - foreach (LogLevel logLevel in logLevels) - { - var item = new MenuItem - { - Title = logLevelMenuStrings [(int)logLevel] - }; - item.CheckType |= MenuItemCheckStyle.Checked; - item.Checked = Enum.Parse (_options.DebugLogLevel) == logLevel; - - item.Action += () => - { - foreach (MenuItem? menuItem in menuItems.Where (mi => mi is { } && logLevelMenuStrings.Contains (mi.Title))) - { - menuItem!.Checked = false; - } - - if (item.Title == logLevelMenuStrings [(int)logLevel] && item.Checked == false) - { - _options.DebugLogLevel = Enum.GetName (logLevel)!; - _logLevelSwitch.MinimumLevel = LogLevelToLogEventLevel (Enum.Parse (_options.DebugLogLevel)); - item.Checked = true; - } - - Diagnostics = _diagnosticFlags; - }; - menuItems.Add (item); - } - - // add a separator - menuItems.Add (null!); - - menuItems.Add ( - new ( - $"_Open Log Folder", - "", - () => OpenUrl (LOGFILE_LOCATION), - null, - null, - null - )); - - return menuItems.ToArray ()!; - } - - // TODO: This should be an ConfigurationManager setting - private MenuItem [] CreateDisabledEnabledMenuBorder () - { - List menuItems = new (); - MiIsMenuBorderDisabled = new () { Title = "Disable Menu _Border" }; - - MiIsMenuBorderDisabled.ShortcutKey = - new Key (MiIsMenuBorderDisabled!.Title!.Substring (14, 1) [0]).WithAlt.WithCtrl.NoShift; - MiIsMenuBorderDisabled.CheckType |= MenuItemCheckStyle.Checked; - - MiIsMenuBorderDisabled.Action += () => - { - MiIsMenuBorderDisabled.Checked = (bool)!MiIsMenuBorderDisabled.Checked!; - - MenuBar!.MenusBorderStyle = !(bool)MiIsMenuBorderDisabled.Checked - ? LineStyle.Single - : LineStyle.None; - }; - menuItems.Add (MiIsMenuBorderDisabled); - - return menuItems.ToArray (); - } - - private MenuItem [] CreateDisabledEnabledMouseItems () - { - List menuItems = new (); - MiIsMouseDisabled = new () { Title = "_Disable Mouse" }; - - MiIsMouseDisabled.ShortcutKey = - new Key (MiIsMouseDisabled!.Title!.Substring (1, 1) [0]).WithAlt.WithCtrl.NoShift; - MiIsMouseDisabled.CheckType |= MenuItemCheckStyle.Checked; - - MiIsMouseDisabled.Action += () => - { - MiIsMouseDisabled.Checked = - Application.IsMouseDisabled = (bool)!MiIsMouseDisabled.Checked!; - }; - menuItems.Add (MiIsMouseDisabled); - - return menuItems.ToArray (); - } - - // TODO: This should be an ConfigurationManager setting - private MenuItem [] CreateDisabledEnableUseSubMenusSingleFrame () - { - List menuItems = new (); - MiUseSubMenusSingleFrame = new () { Title = "Enable _Sub-Menus Single Frame" }; - - MiUseSubMenusSingleFrame.ShortcutKey = KeyCode.CtrlMask - | KeyCode.AltMask - | (KeyCode)MiUseSubMenusSingleFrame!.Title!.Substring (8, 1) [ - 0]; - MiUseSubMenusSingleFrame.CheckType |= MenuItemCheckStyle.Checked; - - MiUseSubMenusSingleFrame.Action += () => - { - MiUseSubMenusSingleFrame.Checked = (bool)!MiUseSubMenusSingleFrame.Checked!; - MenuBar!.UseSubMenusSingleFrame = (bool)MiUseSubMenusSingleFrame.Checked; - }; - menuItems.Add (MiUseSubMenusSingleFrame); - - return menuItems.ToArray (); - } - - private MenuItem [] CreateForce16ColorItems () - { - List menuItems = new (); - - MiForce16Colors = new () - { - Title = "Force _16 Colors", - ShortcutKey = Key.F6, - Checked = Application.Force16Colors, - CanExecute = () => Application.Driver?.SupportsTrueColor ?? false - }; - MiForce16Colors.CheckType |= MenuItemCheckStyle.Checked; - - MiForce16Colors.Action += () => - { - MiForce16Colors.Checked = Application.Force16Colors = (bool)!MiForce16Colors.Checked!; - - ((CheckBox)ShForce16Colors!.CommandView!).CheckedState = - Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked; - Application.LayoutAndDraw (); - }; - menuItems.Add (MiForce16Colors); - - return menuItems.ToArray (); - } - - private MenuItem [] CreateKeyBindingsMenuItems () - { - List menuItems = new (); - var item = new MenuItem { Title = "_Key Bindings", Help = "Change which keys do what" }; - - item.Action += () => - { - var dlg = new KeyBindingsDialog (); - Application.Run (dlg); - dlg.Dispose (); - }; - - menuItems.Add (null!); - menuItems.Add (item); - - return menuItems.ToArray (); - } - - private void LoadedHandler (object? sender, EventArgs? args) - { - ConfigChanged (); - - MiIsMouseDisabled!.Checked = Application.IsMouseDisabled; - - if (ShVersion is { }) - { - ShVersion.Title = $"{RuntimeEnvironment.OperatingSystem} {RuntimeEnvironment.OperatingSystemVersion}, {Driver!.GetVersionInfo ()}"; - } - - if (_selectedScenario != null) - { - _selectedScenario = null; - _isFirstRunning = false; - } - - if (!_isFirstRunning) - { - ScenarioList.SetFocus (); - } - - if (_statusBar is { }) - { - _statusBar.VisibleChanged += (s, e) => { ShowStatusBar = _statusBar.Visible; }; - } - - Loaded -= LoadedHandler; - CategoryList!.EnsureSelectedItemVisible (); - ScenarioList.EnsureSelectedCellIsVisible (); - } - - /// Launches the selected scenario, setting the global _selectedScenario - /// - private void ScenarioView_OpenSelectedItem (object? sender, EventArgs? e) - { - if (_selectedScenario is null) - { - // Save selected item state - _cachedCategoryIndex = CategoryList!.SelectedItem; - _cachedScenarioIndex = ScenarioList.SelectedRow; - - // Create new instance of scenario (even though Scenarios contains instances) - var selectedScenarioName = (string)ScenarioList.Table [ScenarioList.SelectedRow, 0]; - - _selectedScenario = (Scenario)Activator.CreateInstance ( - _scenarios!.FirstOrDefault ( - s => s.GetName () - == selectedScenarioName - )! - .GetType () - )!; - - // Tell the main app to stop - Application.RequestStop (); - } - } - - private void UnloadedHandler (object? sender, EventArgs? args) - { - Applied -= ConfigAppliedHandler; - Unloaded -= UnloadedHandler; - Dispose (); - } - } - - private struct Options - { - public string Driver; - - public string Scenario; - - public uint BenchmarkTimeout; - - public bool Benchmark; - - public string ResultsFile; - - public string DebugLogLevel; - /* etc. */ - } } diff --git a/UICatalog/UICatalog.csproj b/UICatalog/UICatalog.csproj index f0b5a87601..d3a11147d8 100644 --- a/UICatalog/UICatalog.csproj +++ b/UICatalog/UICatalog.csproj @@ -1,6 +1,6 @@  - UICatalog.UICatalogApp + UICatalog.UICatalog Exe diff --git a/UICatalog/UICatalogCommandLineOptions.cs b/UICatalog/UICatalogCommandLineOptions.cs new file mode 100644 index 0000000000..c39b9f2b27 --- /dev/null +++ b/UICatalog/UICatalogCommandLineOptions.cs @@ -0,0 +1,18 @@ +#nullable enable +namespace UICatalog; + +public struct UICatalogCommandLineOptions +{ + public string Driver { get; set; } + + public string Scenario { get; set; } + + public uint BenchmarkTimeout { get; set; } + + public bool Benchmark { get; set; } + + public string ResultsFile { get; set; } + + public string DebugLogLevel { get; set; } + /* etc. */ +} diff --git a/UICatalog/UICatalogTopLevel.cs b/UICatalog/UICatalogTopLevel.cs new file mode 100644 index 0000000000..d6f65cc6f9 --- /dev/null +++ b/UICatalog/UICatalogTopLevel.cs @@ -0,0 +1,721 @@ +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using System.Text; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Logging; +using Terminal.Gui; +using static Terminal.Gui.ConfigurationManager; +using Command = Terminal.Gui.Command; +using RuntimeEnvironment = Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment; + +#nullable enable + +namespace UICatalog; + +/// +/// This is the main UI Catalog app view. It is run fresh when the app loads (if a Scenario has not been passed on +/// the command line) and each time a Scenario ends. +/// +public class UICatalogTopLevel : Toplevel +{ + // When a scenario is run, the main app is killed. The static + // members are cached so that when the scenario exits the + // main app UI can be restored to previous state + + // Theme Management + public static string? CachedTheme { get; set; } + + public static string? CachedTopLevelColorScheme { get; set; } + + // Diagnostics + private static ViewDiagnosticFlags _diagnosticFlags; + + public UICatalogTopLevel () + { + _diagnosticFlags = Diagnostics; + + _menuBar = CreateMenuBar (); + _statusBar = CreateStatusBar (); + _categoryList = CreateCategoryList (); + _scenarioList = CreateScenarioList (); + + Add (_menuBar, _categoryList, _scenarioList, _statusBar); + + Loaded += LoadedHandler; + Unloaded += UnloadedHandler; + + // Restore previous selections + _categoryList.SelectedItem = _cachedCategoryIndex; + _scenarioList.SelectedRow = _cachedScenarioIndex; + + Applied += ConfigAppliedHandler; + } + + + private static bool _isFirstRunning = true; + + private void LoadedHandler (object? sender, EventArgs? args) + { + ConfigChanged (); + + if (_disableMouseCb is { }) + { + _disableMouseCb.CheckedState = Application.IsMouseDisabled ? CheckState.Checked : CheckState.UnChecked; + } + + if (_shVersion is { }) + { + _shVersion.Title = $"{RuntimeEnvironment.OperatingSystem} {RuntimeEnvironment.OperatingSystemVersion}, {Driver!.GetVersionInfo ()}"; + } + + if (CachedSelectedScenario != null) + { + CachedSelectedScenario = null; + _isFirstRunning = false; + } + + if (!_isFirstRunning) + { + _scenarioList.SetFocus (); + } + + if (_statusBar is { }) + { + _statusBar.VisibleChanged += (s, e) => { ShowStatusBar = _statusBar.Visible; }; + } + + Loaded -= LoadedHandler; + _categoryList!.EnsureSelectedItemVisible (); + _scenarioList.EnsureSelectedCellIsVisible (); + } + + private void UnloadedHandler (object? sender, EventArgs? args) + { + Applied -= ConfigAppliedHandler; + Unloaded -= UnloadedHandler; + Dispose (); + } + + #region MenuBar + + private readonly MenuBarv2? _menuBar; + private CheckBox? _force16ColorsMenuItemCb; + private RadioGroup? _themesRg; + private RadioGroup? _topSchemeRg; + private RadioGroup? _logLevelRg; + private FlagSelector? _diagnosticFlagsSelector; + private CheckBox? _disableMouseCb; + + private MenuBarv2 CreateMenuBar () + { + MenuBarv2 menuBar = new ( + [ + new ( + "_File", + [ + new MenuItemv2 ( + "_Quit", + "Quit UI Catalog", + RequestStop + ) + ]), + new ("_Themes", CreateThemeMenuItems ()), + new ("Diag_nostics", CreateDiagnosticMenuItems ()), + new ("_Logging", CreateLoggingMenuItems ()), + new ( + "_Help", + [ + new MenuItemv2 ( + "_Documentation", + "", + () => OpenUrl ("https://gui-cs.github.io/Terminal.GuiV2Docs"), + Key.F1 + ), + new MenuItemv2 ( + "_README", + "", + () => OpenUrl ("https://github.com/gui-cs/Terminal.Gui"), + Key.F2 + ), + new MenuItemv2 ( + "_About...", + "About UI Catalog", + () => MessageBox.Query ( + "", + GetAboutBoxMessage (), + wrapMessage: false, + buttons: "_Ok" + ), + Key.A.WithCtrl + ) + ]) + ]); + + return menuBar; + + View [] CreateThemeMenuItems () + { + List menuItems = []; + + _force16ColorsMenuItemCb = new () + { + Title = "Force _16 Colors", + CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked + }; + + _force16ColorsMenuItemCb.CheckedStateChanged += (sender, args) => + { + Application.Force16Colors = args.CurrentValue == CheckState.Checked; + + _force16ColorsShortcutCb!.CheckedState = args.CurrentValue; + Application.LayoutAndDraw (); + }; + + menuItems.Add ( + new MenuItemv2 + { + CommandView = _force16ColorsMenuItemCb + }); + + menuItems.Add (new Line ()); + + _themesRg = new (); + + _themesRg.SelectedItemChanged += (_, args) => + { + Themes!.Theme = Themes!.Keys.ToArray () [args.SelectedItem]; + CachedTheme = Themes!.Keys.ToArray () [args.SelectedItem]; + Apply (); + SetNeedsDraw (); + }; + + var menuItem = new MenuItemv2 + { + CommandView = _themesRg, + HelpText = "Cycle Through Themes", + Key = Key.T.WithCtrl + }; + menuItems.Add (menuItem); + + menuItems.Add (new Line ()); + + _topSchemeRg = new (); + + _topSchemeRg.SelectedItemChanged += (_, args) => + { + CachedTopLevelColorScheme = Colors.ColorSchemes.Keys.ToArray () [args.SelectedItem]; + ColorScheme = Colors.ColorSchemes [CachedTopLevelColorScheme]; + SetNeedsDraw (); + }; + + menuItem = new () + { + Title = "Color Scheme for Application._Top", + SubMenu = new ( + [ + new () + { + CommandView = _topSchemeRg, + HelpText = "Cycle Through Color Schemes", + Key = Key.S.WithCtrl + } + ]) + }; + menuItems.Add (menuItem); + + UpdateThemesMenu (); + + return menuItems.ToArray (); + } + + View [] CreateDiagnosticMenuItems () + { + List menuItems = []; + + _diagnosticFlagsSelector = new () + { + CanFocus = false, + Styles = FlagSelectorStyles.ShowNone, + HighlightStyle = HighlightStyle.None + }; + _diagnosticFlagsSelector.SetFlags (); + + _diagnosticFlagsSelector.ValueChanged += (sender, args) => + { + _diagnosticFlags = (ViewDiagnosticFlags)_diagnosticFlagsSelector.Value; + Diagnostics = _diagnosticFlags; + }; + + menuItems.Add ( + new MenuItemv2 + { + CommandView = _diagnosticFlagsSelector, + HelpText = "View Diagnostics" + }); + + menuItems.Add (new Line ()); + + _disableMouseCb = new () + { + Title = "_Disable Mouse", + CheckedState = Application.IsMouseDisabled ? CheckState.Checked : CheckState.UnChecked + }; + + _disableMouseCb.CheckedStateChanged += (_, args) => { Application.IsMouseDisabled = args.CurrentValue == CheckState.Checked; }; + + menuItems.Add ( + new MenuItemv2 + { + CommandView = _disableMouseCb, + HelpText = "Disable Mouse" + }); + + return menuItems.ToArray (); + } + + View [] CreateLoggingMenuItems () + { + List menuItems = []; + + LogLevel [] logLevels = Enum.GetValues (); + + _logLevelRg = new () + { + AssignHotKeysToRadioLabels = true, + RadioLabels = Enum.GetNames (), + SelectedItem = logLevels.ToList ().IndexOf (Enum.Parse (UICatalog.Options.DebugLogLevel)) + }; + + _logLevelRg.SelectedItemChanged += (_, args) => + { + UICatalog.Options = UICatalog.Options with { DebugLogLevel = Enum.GetName (logLevels [args.SelectedItem])! }; + + UICatalog.LogLevelSwitch.MinimumLevel = + UICatalog.LogLevelToLogEventLevel (Enum.Parse (UICatalog.Options.DebugLogLevel)); + }; + + menuItems.Add ( + new MenuItemv2 + { + CommandView = _logLevelRg, + HelpText = "Cycle Through Log Levels", + Key = Key.L.WithCtrl + }); + + // add a separator + menuItems.Add (new Line ()); + + menuItems.Add ( + new MenuItemv2 ( + "_Open Log Folder", + string.Empty, + () => OpenUrl (UICatalog.LOGFILE_LOCATION) + )); + + return menuItems.ToArray ()!; + } + + } + + private void UpdateThemesMenu () + { + if (_themesRg is null) + { + return; + } + + _themesRg.AssignHotKeysToRadioLabels = true; + _themesRg.UsedHotKeys.Clear (); + _themesRg.RadioLabels = Themes!.Keys.ToArray (); + _themesRg.SelectedItem = Themes.Keys.ToList ().IndexOf (CachedTheme!.Replace ("_", string.Empty)); + + if (_topSchemeRg is null) + { + return; + } + + _topSchemeRg.AssignHotKeysToRadioLabels = true; + _topSchemeRg.UsedHotKeys.Clear (); + int selected = _topSchemeRg.SelectedItem; + _topSchemeRg.RadioLabels = Colors.ColorSchemes.Keys.ToArray (); + _topSchemeRg.SelectedItem = selected; + + if (CachedTopLevelColorScheme is null || !Colors.ColorSchemes.ContainsKey (CachedTopLevelColorScheme)) + { + CachedTopLevelColorScheme = "Base"; + } + + _topSchemeRg.SelectedItem = Array.IndexOf (Colors.ColorSchemes.Keys.ToArray (), CachedTopLevelColorScheme); + } + + #endregion MenuBar + + #region Scenario List + + private readonly TableView _scenarioList; + + private static int _cachedScenarioIndex; + + public static ObservableCollection? CachedScenarios { get; set; } + + // UI Catalog uses TableView for the scenario list instead of a ListView to demonstrate how + // TableView works. There's no real reason not to use ListView. Because we use TableView, and TableView + // doesn't (currently) have CollectionNavigator support built in, we implement it here, within the app. + private readonly CollectionNavigator _scenarioCollectionNav = new (); + + // If set, holds the scenario the user selected to run + public static Scenario? CachedSelectedScenario { get; set; } + + private TableView CreateScenarioList () + { + // Create the scenario list. The contents of the scenario list changes whenever the + // Category list selection changes (to show just the scenarios that belong to the selected + // category). + TableView scenarioList = new () + { + X = Pos.Right (_categoryList!) - 1, + Y = Pos.Bottom (_menuBar!), + Width = Dim.Fill (), + Height = Dim.Fill ( + Dim.Func ( + () => + { + if (_statusBar!.NeedsLayout) + { + throw new LayoutException ("DimFunc.Fn aborted because dependent View needs layout."); + + //_statusBar.Layout (); + } + + return _statusBar.Frame.Height; + })), + + //AllowsMarking = false, + CanFocus = true, + Title = "_Scenarios", + BorderStyle = _categoryList!.BorderStyle, + SuperViewRendersLineCanvas = true + }; + + // TableView provides many options for table headers. For simplicity, we turn all + // of these off. By enabling FullRowSelect and turning off headers, TableView looks just + // like a ListView + scenarioList.FullRowSelect = true; + scenarioList.Style.ShowHeaders = false; + scenarioList.Style.ShowHorizontalHeaderOverline = false; + scenarioList.Style.ShowHorizontalHeaderUnderline = false; + scenarioList.Style.ShowHorizontalBottomline = false; + scenarioList.Style.ShowVerticalCellLines = false; + scenarioList.Style.ShowVerticalHeaderLines = false; + + /* By default, TableView lays out columns at render time and only + * measures y rows of data at a time. Where y is the height of the + * console. This is for the following reasons: + * + * - Performance, when tables have a large amount of data + * - Defensive, prevents a single wide cell value pushing other + * columns off-screen (requiring horizontal scrolling + * + * In the case of UICatalog here, such an approach is overkill so + * we just measure all the data ourselves and set the appropriate + * max widths as ColumnStyles + */ + int longestName = CachedScenarios!.Max (s => s.GetName ().Length); + + scenarioList.Style.ColumnStyles.Add ( + 0, + new () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName } + ); + scenarioList.Style.ColumnStyles.Add (1, new () { MaxWidth = 1 }); + scenarioList.CellActivated += ScenarioView_OpenSelectedItem; + + // TableView typically is a grid where nav keys are biased for moving left/right. + scenarioList.KeyBindings.Remove (Key.Home); + scenarioList.KeyBindings.Add (Key.Home, Command.Start); + scenarioList.KeyBindings.Remove (Key.End); + scenarioList.KeyBindings.Add (Key.End, Command.End); + + // Ideally, TableView.MultiSelect = false would turn off any keybindings for + // multi-select options. But it currently does not. UI Catalog uses Ctrl-A for + // a shortcut to About. + scenarioList.MultiSelect = false; + scenarioList.KeyBindings.Remove (Key.A.WithCtrl); + + return scenarioList; + } + + + /// Launches the selected scenario, setting the global _selectedScenario + /// + private void ScenarioView_OpenSelectedItem (object? sender, EventArgs? e) + { + if (CachedSelectedScenario is null) + { + // Save selected item state + _cachedCategoryIndex = _categoryList!.SelectedItem; + _cachedScenarioIndex = _scenarioList.SelectedRow; + + // Create new instance of scenario (even though Scenarios contains instances) + var selectedScenarioName = (string)_scenarioList.Table [_scenarioList.SelectedRow, 0]; + + CachedSelectedScenario = (Scenario)Activator.CreateInstance ( + CachedScenarios!.FirstOrDefault ( + s => s.GetName () + == selectedScenarioName + )! + .GetType () + )!; + + // Tell the main app to stop + Application.RequestStop (); + } + } + + #endregion Scenario List + + #region Category List + + private readonly ListView? _categoryList; + private static int _cachedCategoryIndex; + public static ObservableCollection? CachedCategories { get; set; } + + private ListView CreateCategoryList () + { + // Create the Category list view. This list never changes. + ListView categoryList = new () + { + X = 0, + Y = Pos.Bottom (_menuBar!), + Width = Dim.Auto (), + Height = Dim.Fill ( + Dim.Func ( + () => + { + if (_statusBar!.NeedsLayout) + { + throw new LayoutException ("DimFunc.Fn aborted because dependent View needs layout."); + + //_statusBar.Layout (); + } + + return _statusBar.Frame.Height; + })), + AllowsMarking = false, + CanFocus = true, + Title = "_Categories", + BorderStyle = LineStyle.Rounded, + SuperViewRendersLineCanvas = true, + Source = new ListWrapper (CachedCategories) + }; + categoryList.OpenSelectedItem += (s, a) => { _scenarioList!.SetFocus (); }; + categoryList.SelectedItemChanged += CategoryView_SelectedChanged; + + // This enables the scrollbar by causing lazy instantiation to happen + categoryList.VerticalScrollBar.AutoShow = true; + + return categoryList; + } + + private void CategoryView_SelectedChanged (object? sender, ListViewItemEventArgs? e) + { + string item = CachedCategories! [e!.Item]; + ObservableCollection newScenarioList; + + if (e.Item == 0) + { + // First category is "All" + newScenarioList = CachedScenarios!; + } + else + { + newScenarioList = new (CachedScenarios!.Where (s => s.GetCategories ().Contains (item)).ToList ()); + } + + _scenarioList.Table = new EnumerableTableSource ( + newScenarioList, + new () + { + { "Name", s => s.GetName () }, { "Description", s => s.GetDescription () } + } + ); + + // Create a collection of just the scenario names (the 1st column in our TableView) + // for CollectionNavigator. + List firstColumnList = []; + + for (var i = 0; i < _scenarioList.Table.Rows; i++) + { + firstColumnList.Add (_scenarioList.Table [i, 0]); + } + + _scenarioCollectionNav.Collection = firstColumnList; + } + + #endregion Category List + + #region StatusBar + + private readonly StatusBar? _statusBar; + + [SerializableConfigurationProperty (Scope = typeof (AppScope), OmitClassName = true)] + [JsonPropertyName ("UICatalog.StatusBar")] + public static bool ShowStatusBar { get; set; } = true; + + private Shortcut? _shVersion; + private CheckBox? _force16ColorsShortcutCb; + + private StatusBar CreateStatusBar () + { + StatusBar statusBar = new () + { + Visible = ShowStatusBar, + AlignmentModes = AlignmentModes.IgnoreFirstOrLast, + CanFocus = false + }; + + // ReSharper disable All + statusBar.Height = Dim.Auto ( + DimAutoStyle.Auto, + minimumContentDim: Dim.Func (() => statusBar.Visible ? 1 : 0), + maximumContentDim: Dim.Func (() => statusBar.Visible ? 1 : 0)); + // ReSharper restore All + + _shVersion = new () + { + Title = "Version Info", + CanFocus = false + }; + + var statusBarShortcut = new Shortcut + { + Key = Key.F10, + Title = "Show/Hide Status Bar", + CanFocus = false + }; + + statusBarShortcut.Accepting += (sender, args) => + { + statusBar.Visible = !_statusBar!.Visible; + args.Cancel = true; + }; + + _force16ColorsShortcutCb = new () + { + Title = "16 color mode", + CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked, + CanFocus = false + }; + + _force16ColorsShortcutCb.CheckedStateChanging += (sender, args) => + { + Application.Force16Colors = args.NewValue == CheckState.Checked; + _force16ColorsMenuItemCb!.CheckedState = args.NewValue; + Application.LayoutAndDraw (); + }; + + statusBar.Add ( + new Shortcut + { + CanFocus = false, + Title = "Quit", + Key = Application.QuitKey + }, + statusBarShortcut, + new Shortcut + { + CanFocus = false, + CommandView = _force16ColorsShortcutCb, + HelpText = "", + BindKeyToApplication = true, + Key = Key.F7 + }, + _shVersion + ); + + return statusBar; + } + + #endregion StatusBar + + #region Configuration Manager + public void ConfigChanged () + { + CachedTheme = Themes?.Theme; + + UpdateThemesMenu (); + + ColorScheme = Colors.ColorSchemes [CachedTopLevelColorScheme!]; + + ((Shortcut)_statusBar!.SubViews.ElementAt (0)).Key = Application.QuitKey; + _statusBar.Visible = ShowStatusBar; + + _disableMouseCb!.CheckedState = Application.IsMouseDisabled ? CheckState.Checked : CheckState.UnChecked; + _force16ColorsShortcutCb!.CheckedState = Application.Force16Colors ? CheckState.Checked : CheckState.UnChecked; + + Application.Top!.SetNeedsDraw (); + } + + private void ConfigAppliedHandler (object? sender, ConfigurationManagerEventArgs? a) { ConfigChanged (); } + + #endregion Configuration Manager + + /// + /// Gets the message displayed in the About Box. `public` so it can be used from Unit tests. + /// + /// + public static string GetAboutBoxMessage () + { + // NOTE: Do not use multiline verbatim strings here. + // WSL gets all confused. + StringBuilder msg = new (); + msg.AppendLine ("UI Catalog: A comprehensive sample library and test app for"); + msg.AppendLine (); + + msg.AppendLine ( + """ + _______ _ _ _____ _ + |__ __| (_) | | / ____| (_) + | | ___ _ __ _ __ ___ _ _ __ __ _| || | __ _ _ _ + | |/ _ \ '__| '_ ` _ \| | '_ \ / _` | || | |_ | | | | | + | | __/ | | | | | | | | | | | (_| | || |__| | |_| | | + |_|\___|_| |_| |_| |_|_|_| |_|\__,_|_(_)_____|\__,_|_| + """); + msg.AppendLine (); + msg.AppendLine ("v2 - Pre-Alpha"); + msg.AppendLine (); + msg.AppendLine ("https://github.com/gui-cs/Terminal.Gui"); + + return msg.ToString (); + } + + public static void OpenUrl (string url) + { + if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) + { + url = url.Replace ("&", "^&"); + Process.Start (new ProcessStartInfo ("cmd", $"/c start {url}") { CreateNoWindow = true }); + } + else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) + { + using var process = new Process + { + StartInfo = new () + { + FileName = "xdg-open", + Arguments = url, + RedirectStandardError = true, + RedirectStandardOutput = true, + CreateNoWindow = true, + UseShellExecute = false + } + }; + process.Start (); + } + else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) + { + Process.Start ("open", url); + } + } +} From 4d87d5f249838e280d0887ce435580087b85885b Mon Sep 17 00:00:00 2001 From: Thomas Nind <31306100+tznind@users.noreply.github.com> Date: Sat, 5 Apr 2025 13:47:39 +0100 Subject: [PATCH 06/28] Fixes #4009 - fix tree ordering (#4015) --- .../Views/TableView/TreeTableSource.cs | 4 +- Terminal.Gui/Views/TreeView/Branch.cs | 180 +++++++++--------- Terminal.Gui/Views/TreeView/TreeView.cs | 10 +- Terminal.sln | 6 + .../TerminalGuiFluentTesting.Xunit.csproj | 14 ++ .../XunitContextExtensions.cs | 25 +++ TerminalGuiFluentTesting/GuiTestContext.cs | 35 +++- .../FluentTests/BasicFluentAssertionTests.cs | 14 +- .../FluentTests/TestOutputWriter.cs | 15 ++ .../FluentTests/TreeViewFluentTests.cs | 162 ++++++++++++++++ .../IntegrationTests/IntegrationTests.csproj | 1 + 11 files changed, 356 insertions(+), 110 deletions(-) create mode 100644 TerminalGuiFluentTesting.Xunit/TerminalGuiFluentTesting.Xunit.csproj create mode 100644 TerminalGuiFluentTesting.Xunit/XunitContextExtensions.cs create mode 100644 Tests/IntegrationTests/FluentTests/TestOutputWriter.cs create mode 100644 Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs diff --git a/Terminal.Gui/Views/TableView/TreeTableSource.cs b/Terminal.Gui/Views/TableView/TreeTableSource.cs index 9125c0c953..06b304634a 100644 --- a/Terminal.Gui/Views/TableView/TreeTableSource.cs +++ b/Terminal.Gui/Views/TableView/TreeTableSource.cs @@ -87,8 +87,8 @@ private string GetColumnZeroRepresentationFromTree (int row) Branch branch = RowToBranch (row); // Everything on line before the expansion run and branch text - Rune [] prefix = branch.GetLinePrefix (Application.Driver).ToArray (); - Rune expansion = branch.GetExpandableSymbol (Application.Driver); + Rune [] prefix = branch.GetLinePrefix ().ToArray (); + Rune expansion = branch.GetExpandableSymbol (); string lineBody = _tree.AspectGetter (branch.Model) ?? ""; var sb = new StringBuilder (); diff --git a/Terminal.Gui/Views/TreeView/Branch.cs b/Terminal.Gui/Views/TreeView/Branch.cs index e7a5eb4ca4..4c348eaff6 100644 --- a/Terminal.Gui/Views/TreeView/Branch.cs +++ b/Terminal.Gui/Views/TreeView/Branch.cs @@ -1,8 +1,10 @@ -namespace Terminal.Gui; +#nullable enable + +namespace Terminal.Gui; internal class Branch where T : class { - private readonly TreeView tree; + private readonly TreeView _tree; /// /// Declares a new branch of in which the users object is @@ -11,9 +13,9 @@ internal class Branch where T : class /// The UI control in which the branch resides. /// Pass null for root level branches, otherwise pass the parent. /// The user's object that should be displayed. - public Branch (TreeView tree, Branch parentBranchIfAny, T model) + public Branch (TreeView tree, Branch? parentBranchIfAny, T model) { - this.tree = tree; + _tree = tree; Model = model; if (parentBranchIfAny is { }) @@ -27,7 +29,7 @@ public Branch (TreeView tree, Branch parentBranchIfAny, T model) /// The children of the current branch. This is null until the first call to to avoid /// enumerating the entire underlying hierarchy. /// - public Dictionary> ChildBranches { get; set; } + public List>? ChildBranches { get; set; } /// The depth of the current branch. Depth of 0 indicates root level branches. public int Depth { get; } @@ -39,7 +41,7 @@ public Branch (TreeView tree, Branch parentBranchIfAny, T model) public T Model { get; private set; } /// The parent or null if it is a root. - public Branch Parent { get; } + public Branch? Parent { get; } /// /// Returns true if the current branch can be expanded according to the or cached @@ -52,13 +54,13 @@ public bool CanExpand () if (ChildBranches is null) { //if there is a rapid method for determining whether there are children - if (tree.TreeBuilder.SupportsCanExpand) + if (_tree.TreeBuilder.SupportsCanExpand) { - return tree.TreeBuilder.CanExpand (Model); + return _tree.TreeBuilder.CanExpand (Model); } //there is no way of knowing whether we can expand without fetching the children - FetchChildren (); + ChildBranches = FetchChildren (); } //we fetched or already know the children, so return whether we have any @@ -69,32 +71,30 @@ public bool CanExpand () public void Collapse () { IsExpanded = false; } /// Renders the current on the specified line . - /// - /// /// /// - public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y, int availableWidth) + public virtual void Draw (int y, int availableWidth) { List cells = new (); int? indexOfExpandCollapseSymbol = null; int indexOfModelText; // true if the current line of the tree is the selected one and control has focus - bool isSelected = tree.IsSelected (Model); + bool isSelected = _tree.IsSelected (Model); Attribute textColor = - isSelected ? tree.HasFocus ? colorScheme.Focus : colorScheme.HotNormal : colorScheme.Normal; - Attribute symbolColor = tree.Style.HighlightModelTextOnly ? colorScheme.Normal : textColor; + isSelected ? _tree.HasFocus ? _tree.GetFocusColor () : _tree.GetHotNormalColor () : _tree.GetNormalColor (); + Attribute symbolColor = _tree.Style.HighlightModelTextOnly ? _tree.GetNormalColor () : textColor; // Everything on line before the expansion run and branch text - Rune [] prefix = GetLinePrefix (driver).ToArray (); - Rune expansion = GetExpandableSymbol (driver); - string lineBody = tree.AspectGetter (Model) ?? ""; + Rune [] prefix = GetLinePrefix ().ToArray (); + Rune expansion = GetExpandableSymbol (); + string lineBody = _tree.AspectGetter (Model) ?? ""; - tree.Move (0, y); + _tree.Move (0, y); // if we have scrolled to the right then bits of the prefix will have disappeared off the screen - int toSkip = tree.ScrollOffsetHorizontal; + int toSkip = _tree.ScrollOffsetHorizontal; Attribute attr = symbolColor; // Draw the line prefix (all parallel lanes or whitespace and an expand/collapse/leaf symbol) @@ -112,20 +112,20 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y, } // pick color for expanded symbol - if (tree.Style.ColorExpandSymbol || tree.Style.InvertExpandSymbolColors) + if (_tree.Style.ColorExpandSymbol || _tree.Style.InvertExpandSymbolColors) { - Attribute color = symbolColor; + Attribute color; - if (tree.Style.ColorExpandSymbol) + if (_tree.Style.ColorExpandSymbol) { if (isSelected) { - color = tree.Style.HighlightModelTextOnly ? colorScheme.HotNormal : - tree.HasFocus ? tree.ColorScheme.HotFocus : tree.ColorScheme.HotNormal; + color = _tree.Style.HighlightModelTextOnly ? _tree.GetHotNormalColor () : + _tree.HasFocus ? _tree.GetHotFocusColor () : _tree.GetHotNormalColor (); } else { - color = tree.ColorScheme.HotNormal; + color = _tree.GetHotNormalColor (); } } else @@ -133,9 +133,9 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y, color = symbolColor; } - if (tree.Style.InvertExpandSymbolColors) + if (_tree.Style.InvertExpandSymbolColors) { - color = new Attribute (color.Background, color.Foreground); + color = new (color.Background, color.Foreground); } attr = color; @@ -177,10 +177,10 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y, if (lineBody.EnumerateRunes ().Sum (l => l.GetColumns ()) > availableWidth) { // remaining space is zero and truncate the line - lineBody = new string ( - lineBody.TakeWhile (c => (availableWidth -= ((Rune)c).GetColumns ()) >= 0) - .ToArray () - ); + lineBody = new ( + lineBody.TakeWhile (c => (availableWidth -= ((Rune)c).GetColumns ()) >= 0) + .ToArray () + ); availableWidth = 0; } else @@ -194,9 +194,9 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y, Attribute modelColor = textColor; // if custom color delegate invoke it - if (tree.ColorGetter is { }) + if (_tree.ColorGetter is { }) { - ColorScheme modelScheme = tree.ColorGetter (Model); + ColorScheme modelScheme = _tree.ColorGetter (Model); // if custom color scheme is defined for this Model if (modelScheme is { }) @@ -206,12 +206,12 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y, } else { - modelColor = new Attribute (); + modelColor = new (); } } attr = modelColor; - cells.AddRange (lineBody.Select (r => NewCell (attr, new Rune (r)))); + cells.AddRange (lineBody.Select (r => NewCell (attr, new (r)))); if (availableWidth > 0) { @@ -219,7 +219,7 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y, cells.AddRange ( Enumerable.Repeat ( - NewCell (attr, new Rune (' ')), + NewCell (attr, new (' ')), availableWidth ) ); @@ -230,32 +230,29 @@ public virtual void Draw (IConsoleDriver driver, ColorScheme colorScheme, int y, Model = Model, Y = y, Cells = cells, - Tree = tree, + Tree = _tree, IndexOfExpandCollapseSymbol = indexOfExpandCollapseSymbol, IndexOfModelText = indexOfModelText }; - tree.OnDrawLine (e); + _tree.OnDrawLine (e); - if (!e.Handled && driver != null) + if (!e.Handled) { foreach (Cell cell in cells) { - driver.SetAttribute ((Attribute)cell.Attribute!); - driver.AddRune (cell.Rune); + _tree.SetAttribute ((Attribute)cell.Attribute!); + _tree.AddRune (cell.Rune); } } - driver?.SetAttribute (colorScheme.Normal); + _tree.SetAttribute (_tree.GetNormalColor()); } /// Expands the current branch if possible. public void Expand () { - if (ChildBranches is null) - { - FetchChildren (); - } + ChildBranches ??= FetchChildren (); if (ChildBranches.Any ()) { @@ -264,45 +261,44 @@ public void Expand () } /// Fetch the children of this branch. This method populates . - public virtual void FetchChildren () + private List> FetchChildren () { - if (tree.TreeBuilder is null) + if (_tree.TreeBuilder is null) { - return; + return []; } IEnumerable children; - if (Depth >= tree.MaxDepth) + if (Depth >= _tree.MaxDepth) { - children = Enumerable.Empty (); + children = []; } else { - children = tree.TreeBuilder.GetChildren (Model) ?? Enumerable.Empty (); + children = _tree.TreeBuilder.GetChildren (Model) ?? []; } - ChildBranches = children.ToDictionary (k => k, val => new Branch (tree, this, val)); + return children.Select (o => new Branch (_tree, this, o)).ToList (); } /// /// Returns an appropriate symbol for displaying next to the string representation of the /// object to indicate whether it or not (or it is a leaf). /// - /// /// - public Rune GetExpandableSymbol (IConsoleDriver driver) + public Rune GetExpandableSymbol () { - Rune leafSymbol = tree.Style.ShowBranchLines ? Glyphs.HLine : (Rune)' '; + Rune leafSymbol = _tree.Style.ShowBranchLines ? Glyphs.HLine : (Rune)' '; if (IsExpanded) { - return tree.Style.CollapseableSymbol ?? leafSymbol; + return _tree.Style.CollapseableSymbol ?? leafSymbol; } if (CanExpand ()) { - return tree.Style.ExpandableSymbol ?? leafSymbol; + return _tree.Style.ExpandableSymbol ?? leafSymbol; } return leafSymbol; @@ -313,10 +309,10 @@ public Rune GetExpandableSymbol (IConsoleDriver driver) /// line body). /// /// - public virtual int GetWidth (IConsoleDriver driver) + public virtual int GetWidth () { return - GetLinePrefix (driver).Sum (r => r.GetColumns ()) + GetExpandableSymbol (driver).GetColumns () + (tree.AspectGetter (Model) ?? "").Length; + GetLinePrefix ().Sum (r => r.GetColumns ()) + GetExpandableSymbol ().GetColumns () + (_tree.AspectGetter (Model) ?? "").Length; } /// Refreshes cached knowledge in this branch e.g. what children an object has. @@ -333,41 +329,46 @@ public void Refresh (bool startAtTop) //if we don't know about any children yet just use the normal method if (ChildBranches is null) { - FetchChildren (); + ChildBranches = FetchChildren (); } else { // we already knew about some children so preserve the state of the old children // first gather the new Children - IEnumerable newChildren = tree.TreeBuilder?.GetChildren (Model) ?? Enumerable.Empty (); + T [] newChildren = _tree.TreeBuilder?.GetChildren (Model).ToArray () ?? []; // Children who no longer appear need to go - foreach (T toRemove in ChildBranches.Keys.Except (newChildren).ToArray ()) + foreach (Branch toRemove in ChildBranches.Where (b => !newChildren.Contains (b.Model)).ToArray ()) { ChildBranches.Remove (toRemove); //also if the user has this node selected (its disappearing) so lets change selection to us (the parent object) to be helpful - if (Equals (tree.SelectedObject, toRemove)) + if (Equals (_tree.SelectedObject, toRemove.Model)) { - tree.SelectedObject = Model; + _tree.SelectedObject = Model; } } // New children need to be added foreach (T newChild in newChildren) { + Branch? existingBranch = ChildBranches.FirstOrDefault (b => b.Model.Equals (newChild)); + // If we don't know about the child, yet we need a new branch - if (!ChildBranches.ContainsKey (newChild)) + if (existingBranch == null) { - ChildBranches.Add (newChild, new Branch (tree, this, newChild)); + ChildBranches.Add (new (_tree, this, newChild)); } else { //we already have this object but update the reference anyway in case Equality match but the references are new - ChildBranches [newChild].Model = newChild; + existingBranch.Model = newChild; } } + + // Order the list + ChildBranches = ChildBranches.OrderBy (b => newChildren.IndexOf (b.Model)).ToList (); } } @@ -381,9 +382,9 @@ internal void CollapseAll () if (ChildBranches is { }) { - foreach (KeyValuePair> child in ChildBranches) + foreach (Branch child in ChildBranches) { - child.Value.CollapseAll (); + child.CollapseAll (); } } } @@ -395,9 +396,9 @@ internal void ExpandAll () if (ChildBranches is { }) { - foreach (KeyValuePair> child in ChildBranches) + foreach (Branch child in ChildBranches) { - child.Value.ExpandAll (); + child.ExpandAll (); } } } @@ -406,16 +407,15 @@ internal void ExpandAll () /// Gets all characters to render prior to the current branches line. This includes indentation whitespace and /// any tree branches (if enabled). /// - /// /// - internal IEnumerable GetLinePrefix (IConsoleDriver driver) + internal IEnumerable GetLinePrefix () { // If not showing line branches or this is a root object. - if (!tree.Style.ShowBranchLines) + if (!_tree.Style.ShowBranchLines) { for (var i = 0; i < Depth; i++) { - yield return new Rune (' '); + yield return new (' '); } yield break; @@ -426,14 +426,14 @@ internal IEnumerable GetLinePrefix (IConsoleDriver driver) { if (cur.IsLast ()) { - yield return new Rune (' '); + yield return new (' '); } else { yield return Glyphs.VLine; } - yield return new Rune (' '); + yield return new (' '); } if (IsLast ()) @@ -462,15 +462,15 @@ internal bool IsHitOnExpandableSymbol (IConsoleDriver driver, int x) } // if we could theoretically expand - if (!IsExpanded && tree.Style.ExpandableSymbol != default (Rune?)) + if (!IsExpanded && _tree.Style.ExpandableSymbol != default (Rune?)) { - return x == GetLinePrefix (driver).Count (); + return x == GetLinePrefix ().Count (); } // if we could theoretically collapse - if (IsExpanded && tree.Style.CollapseableSymbol != default (Rune?)) + if (IsExpanded && _tree.Style.CollapseableSymbol != default (Rune?)) { - return x == GetLinePrefix (driver).Count (); + return x == GetLinePrefix ().Count (); } return false; @@ -487,9 +487,9 @@ internal void Rebuild () if (IsExpanded) { // if we are expanded we need to update the visible children - foreach (KeyValuePair> child in ChildBranches) + foreach (Branch child in ChildBranches) { - child.Value.Rebuild (); + child.Rebuild (); } } else @@ -504,7 +504,7 @@ internal void Rebuild () /// private IEnumerable> GetParentBranches () { - Branch cur = Parent; + Branch? cur = Parent; while (cur is { }) { @@ -523,11 +523,13 @@ private bool IsLast () { if (Parent is null) { - return this == tree.roots.Values.LastOrDefault (); + return this == _tree.roots.Values.LastOrDefault (); } - return Parent.ChildBranches.Values.LastOrDefault () == this; + Parent.ChildBranches ??= Parent.FetchChildren (); + + return Parent.ChildBranches.LastOrDefault () == this; } - private static Cell NewCell (Attribute attr, Rune r) { return new Cell { Rune = r, Attribute = new (attr) }; } + private static Cell NewCell (Attribute attr, Rune r) { return new() { Rune = r, Attribute = new (attr) }; } } diff --git a/Terminal.Gui/Views/TreeView/TreeView.cs b/Terminal.Gui/Views/TreeView/TreeView.cs index 4ff0a3c89f..af2245efae 100644 --- a/Terminal.Gui/Views/TreeView/TreeView.cs +++ b/Terminal.Gui/Views/TreeView/TreeView.cs @@ -847,7 +847,7 @@ public IEnumerable GetChildren (T o) return new T [0]; } - return branch.ChildBranches?.Values?.Select (b => b.Model)?.ToArray () ?? new T [0]; + return branch.ChildBranches?.Select (b => b.Model)?.ToArray () ?? new T [0]; } /// Returns the maximum width line in the tree including prefix and expansion symbols. @@ -879,10 +879,10 @@ public int GetContentWidth (bool visible) return 0; } - return map.Skip (ScrollOffsetVertical).Take (Viewport.Height).Max (b => b.GetWidth (Driver)); + return map.Skip (ScrollOffsetVertical).Take (Viewport.Height).Max (b => b.GetWidth ()); } - return map.Max (b => b.GetWidth (Driver)); + return map.Max (b => b.GetWidth ()); } /// @@ -1171,7 +1171,7 @@ protected override bool OnDrawingContent () if (idxToRender < map.Count) { // Render the line - map.ElementAt (idxToRender).Draw (Driver, ColorScheme, line, Viewport.Width); + map.ElementAt (idxToRender).Draw (line, Viewport.Width); } else { @@ -1488,7 +1488,7 @@ private IEnumerable> AddToLineMap (Branch currentBranch, bool paren if (currentBranch.IsExpanded) { - foreach (Branch subBranch in currentBranch.ChildBranches.Values) + foreach (Branch subBranch in currentBranch.ChildBranches) { foreach (Branch sub in AddToLineMap (subBranch, weMatch, out bool childMatch)) { diff --git a/Terminal.sln b/Terminal.sln index e15d8f3606..b255d94598 100644 --- a/Terminal.sln +++ b/Terminal.sln @@ -65,6 +65,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests.Parallelizable", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTesting", "TerminalGuiFluentTesting\TerminalGuiFluentTesting.csproj", "{2DBA7BDC-17AE-474B-A507-00807D087607}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTesting.Xunit", "TerminalGuiFluentTesting.Xunit\TerminalGuiFluentTesting.Xunit.csproj", "{231B9723-10F3-46DB-8EAE-50C0C0375AD3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -123,6 +125,10 @@ Global {2DBA7BDC-17AE-474B-A507-00807D087607}.Debug|Any CPU.Build.0 = Debug|Any CPU {2DBA7BDC-17AE-474B-A507-00807D087607}.Release|Any CPU.ActiveCfg = Release|Any CPU {2DBA7BDC-17AE-474B-A507-00807D087607}.Release|Any CPU.Build.0 = Release|Any CPU + {231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/TerminalGuiFluentTesting.Xunit/TerminalGuiFluentTesting.Xunit.csproj b/TerminalGuiFluentTesting.Xunit/TerminalGuiFluentTesting.Xunit.csproj new file mode 100644 index 0000000000..03c8b09d94 --- /dev/null +++ b/TerminalGuiFluentTesting.Xunit/TerminalGuiFluentTesting.Xunit.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/TerminalGuiFluentTesting.Xunit/XunitContextExtensions.cs b/TerminalGuiFluentTesting.Xunit/XunitContextExtensions.cs new file mode 100644 index 0000000000..53f81e37b3 --- /dev/null +++ b/TerminalGuiFluentTesting.Xunit/XunitContextExtensions.cs @@ -0,0 +1,25 @@ +using Xunit; + +namespace TerminalGuiFluentTesting; + +public static class XunitContextExtensions +{ + public static GuiTestContext AssertTrue (this GuiTestContext context, bool? condition) + { + context.Then ( + () => + { + Assert.True (condition); + }); + return context; + } + public static GuiTestContext AssertEqual (this GuiTestContext context, object? expected, object? actual) + { + context.Then ( + () => + { + Assert.Equal (expected,actual); + }); + return context; + } +} diff --git a/TerminalGuiFluentTesting/GuiTestContext.cs b/TerminalGuiFluentTesting/GuiTestContext.cs index bd3f8d974d..1a3274cd32 100644 --- a/TerminalGuiFluentTesting/GuiTestContext.cs +++ b/TerminalGuiFluentTesting/GuiTestContext.cs @@ -243,7 +243,18 @@ public GuiTestContext WaitIteration (Action? a = null) /// public GuiTestContext Then (Action doAction) { - doAction (); + try + { + doAction (); + } + catch(Exception) + { + Stop (); + _hardStop.Cancel(); + + throw; + + } return this; } @@ -360,6 +371,7 @@ public GuiTestContext Right () { SendNetKey (k); } + WaitIteration (); break; default: throw new ArgumentOutOfRangeException (); @@ -550,4 +562,25 @@ private void SendWindowsKey (ConsoleKeyMapping.VK specialKey) WaitIteration (); } + + /// + /// Sets the input focus to the given . + /// Throws if focus did not change due to system + /// constraints e.g. + /// is + /// + /// + /// + /// + public GuiTestContext Focus (View toFocus) + { + toFocus.FocusDeepest (NavigationDirection.Forward, TabBehavior.TabStop); + + if (!toFocus.HasFocus) + { + throw new ArgumentException ("Failed to set focus, FocusDeepest did not result in HasFocus becoming true. Ensure view is added and focusable"); + } + + return WaitIteration (); + } } diff --git a/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs b/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs index 93dff6a865..f79325319b 100644 --- a/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs +++ b/Tests/IntegrationTests/FluentTests/BasicFluentAssertionTests.cs @@ -1,5 +1,4 @@ -using System.Text; -using Terminal.Gui; +using Terminal.Gui; using TerminalGuiFluentTesting; using Xunit.Abstractions; @@ -9,17 +8,6 @@ public class BasicFluentAssertionTests { private readonly TextWriter _out; - public class TestOutputWriter : TextWriter - { - private readonly ITestOutputHelper _output; - - public TestOutputWriter (ITestOutputHelper output) { _output = output; } - - public override void WriteLine (string? value) { _output.WriteLine (value ?? string.Empty); } - - public override Encoding Encoding => Encoding.UTF8; - } - public BasicFluentAssertionTests (ITestOutputHelper outputHelper) { _out = new TestOutputWriter (outputHelper); } [Theory] diff --git a/Tests/IntegrationTests/FluentTests/TestOutputWriter.cs b/Tests/IntegrationTests/FluentTests/TestOutputWriter.cs new file mode 100644 index 0000000000..62e40e5aea --- /dev/null +++ b/Tests/IntegrationTests/FluentTests/TestOutputWriter.cs @@ -0,0 +1,15 @@ +using System.Text; +using Xunit.Abstractions; + +namespace IntegrationTests.FluentTests; + +public class TestOutputWriter : TextWriter +{ + private readonly ITestOutputHelper _output; + + public TestOutputWriter (ITestOutputHelper output) { _output = output; } + + public override void WriteLine (string? value) { _output.WriteLine (value ?? string.Empty); } + + public override Encoding Encoding => Encoding.UTF8; +} diff --git a/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs b/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs new file mode 100644 index 0000000000..f735147973 --- /dev/null +++ b/Tests/IntegrationTests/FluentTests/TreeViewFluentTests.cs @@ -0,0 +1,162 @@ +using Terminal.Gui; +using TerminalGuiFluentTesting; +using Xunit.Abstractions; + +namespace IntegrationTests.FluentTests; + +public class TreeViewFluentTests +{ + private readonly TextWriter _out; + + public TreeViewFluentTests (ITestOutputHelper outputHelper) { _out = new TestOutputWriter (outputHelper); } + + [Theory] + [ClassData (typeof (V2TestDrivers))] + public void TreeView_AllowReOrdering (V2TestDriver d) + { + var tv = new TreeView + { + Width = Dim.Fill (), + Height = Dim.Fill () + }; + + TreeNode car; + TreeNode lorry; + TreeNode bike; + + var root = new TreeNode ("Root") + { + Children = + [ + car = new ("Car"), + lorry = new ("Lorry"), + bike = new ("Bike") + ] + }; + + tv.AddObject (root); + + using GuiTestContext context = + With.A (40, 10, d) + .Add (tv) + .Focus (tv) + .WaitIteration () + .ScreenShot ("Before expanding", _out) + .AssertEqual (root, tv.GetObjectOnRow (0)) + .Then (() => Assert.Null (tv.GetObjectOnRow (1))) + .Right () + .ScreenShot ("After expanding", _out) + .AssertEqual (root, tv.GetObjectOnRow (0)) + .AssertEqual (car, tv.GetObjectOnRow (1)) + .AssertEqual (lorry, tv.GetObjectOnRow (2)) + .AssertEqual (bike, tv.GetObjectOnRow (3)) + .Then ( + () => + { + // Re order + root.Children = [bike, car, lorry]; + tv.RefreshObject (root); + }) + .WaitIteration () + .ScreenShot ("After re-order", _out) + .AssertEqual (root, tv.GetObjectOnRow (0)) + .AssertEqual (bike, tv.GetObjectOnRow (1)) + .AssertEqual (car, tv.GetObjectOnRow (2)) + .AssertEqual (lorry, tv.GetObjectOnRow (3)) + .WriteOutLogs (_out); + + context.Stop (); + } + + [Theory] + [ClassData (typeof (V2TestDrivers))] + public void TreeViewReOrder_PreservesExpansion (V2TestDriver d) + { + var tv = new TreeView + { + Width = Dim.Fill (), + Height = Dim.Fill () + }; + + TreeNode car; + TreeNode lorry; + TreeNode bike; + + TreeNode mrA; + TreeNode mrB; + + TreeNode mrC; + + TreeNode mrD; + TreeNode mrE; + + var root = new TreeNode ("Root") + { + Children = + [ + car = new ("Car") + { + Children = + [ + mrA = new ("Mr A"), + mrB = new ("Mr B") + ] + }, + lorry = new ("Lorry") + { + Children = + [ + mrC = new ("Mr C") + ] + }, + bike = new ("Bike") + { + Children = + [ + mrD = new ("Mr D"), + mrE = new ("Mr E") + ] + } + ] + }; + + tv.AddObject (root); + tv.ExpandAll (); + + using GuiTestContext context = + With.A (40, 13, d) + .Add (tv) + .WaitIteration () + .ScreenShot ("Initial State", _out) + .AssertEqual (root, tv.GetObjectOnRow (0)) + .AssertEqual (car, tv.GetObjectOnRow (1)) + .AssertEqual (mrA, tv.GetObjectOnRow (2)) + .AssertEqual (mrB, tv.GetObjectOnRow (3)) + .AssertEqual (lorry, tv.GetObjectOnRow (4)) + .AssertEqual (mrC, tv.GetObjectOnRow (5)) + .AssertEqual (bike, tv.GetObjectOnRow (6)) + .AssertEqual (mrD, tv.GetObjectOnRow (7)) + .AssertEqual (mrE, tv.GetObjectOnRow (8)) + .Then ( + () => + { + // Re order + root.Children = [bike, car, lorry]; + tv.RefreshObject (root); + }) + .WaitIteration () + .ScreenShot ("After re-order", _out) + .AssertEqual (root, tv.GetObjectOnRow (0)) + .AssertEqual (bike, tv.GetObjectOnRow (1)) + .AssertEqual (mrD, tv.GetObjectOnRow (2)) + .AssertEqual (mrE, tv.GetObjectOnRow (3)) + .AssertEqual (car, tv.GetObjectOnRow (4)) + .AssertEqual (mrA, tv.GetObjectOnRow (5)) + .AssertEqual (mrB, tv.GetObjectOnRow (6)) + .AssertEqual (lorry, tv.GetObjectOnRow (7)) + .AssertEqual (mrC, tv.GetObjectOnRow (8)) + .WriteOutLogs (_out); + + context.Stop (); + } +} diff --git a/Tests/IntegrationTests/IntegrationTests.csproj b/Tests/IntegrationTests/IntegrationTests.csproj index f279e21df2..80f067bf7d 100644 --- a/Tests/IntegrationTests/IntegrationTests.csproj +++ b/Tests/IntegrationTests/IntegrationTests.csproj @@ -26,6 +26,7 @@ + From eaa9ee1ef6f29c5edbca5c81c9d7e57b004c499b Mon Sep 17 00:00:00 2001 From: Thomas Nind <31306100+tznind@users.noreply.github.com> Date: Wed, 16 Apr 2025 16:25:07 +0100 Subject: [PATCH 07/28] Fixes #4022 file dialog tests and bugfix for cancellation (#4024) * Add class for detecting information about console in extensible way * WIP - Create test for reordering * Change Dictionary to List and preserve TreeBuilder order * Add test to ensure branch expansion/status remains consistent despite reorder * Cleanup code * Fix regression when removed child was the selected one * Revert "Add class for detecting information about console in extensible way" This reverts commit 7e4253cf28428ea80a4773b137d4bd89cf321746. * Code cleanup and enable nullable on Branch * Remove color scheme and driver from Branch draw * Add xunit context extensions * Investigate codegen for xunit * Getting closer to something that works * Fix code generation * Further explore code gen * Generate all methods in single class for easier extensibility * Simplify code gen by moving parameter creation to its own method * Implement asserts A-I * Add remaining assert calls that are not obsolete * Fix unit test * Roll back versions to be compatible with CI version of csharp * Handle params and ref etc * Fix null warning * WIP - start to add integration tests for FileDialog * Add ability to tab focus to specific control with simple one line delegate * Clarify test criteria * Add unit tests for Ok and other ways of canceling dialog * Fix other buttons also triggering save * Fix for linux environment tests * Fix for linux again * Fix application null race condition - add better way of knowing if stuff is finished * Better fix for shutdown detection * Add test that shows #4026 is not an issue * Switch to `_fileSystem.Directory.GetLogicalDrives ()` * Don't show duplicate MyDocuments etc --- Directory.Packages.props | 93 +++-- Terminal.Gui/FileServices/FileDialogStyle.cs | 9 +- Terminal.Gui/Views/FileDialog.cs | 26 +- Terminal.Gui/Views/SaveDialog.cs | 10 +- Terminal.sln | 16 +- TerminalGuiFluentTesting/GuiTestContext.cs | 229 +++++++++++- TerminalGuiFluentTesting/With.cs | 15 + ...inalGuiFluentTestingXunit.Generator.csproj | 20 ++ .../TheGenerator.cs | 333 ++++++++++++++++++ .../TerminalGuiFluentTestingXunit.csproj | 17 + .../XunitContextExtensions.cs | 9 + .../FluentTests/FileDialogFluentTests.cs | 197 +++++++++++ .../FluentTests/TreeViewFluentTests.cs | 71 ++-- .../IntegrationTests/IntegrationTests.csproj | 2 +- 14 files changed, 946 insertions(+), 101 deletions(-) create mode 100644 TerminalGuiFluentTestingXunit.Generator/TerminalGuiFluentTestingXunit.Generator.csproj create mode 100644 TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs create mode 100644 TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj create mode 100644 TerminalGuiFluentTestingXunit/XunitContextExtensions.cs create mode 100644 Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 18afbe64a4..1a0afff7a4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,52 +1,45 @@ - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Terminal.Gui/FileServices/FileDialogStyle.cs b/Terminal.Gui/FileServices/FileDialogStyle.cs index aac806de81..34804775ee 100644 --- a/Terminal.Gui/FileServices/FileDialogStyle.cs +++ b/Terminal.Gui/FileServices/FileDialogStyle.cs @@ -151,14 +151,11 @@ private Dictionary DefaultTreeRootGetter () try { - foreach (string d in GetLogicalDrives ()) + foreach (string d in _fileSystem.Directory.GetLogicalDrives ()) { IDirectoryInfo dir = _fileSystem.DirectoryInfo.New (d); - if (!roots.ContainsKey (dir)) - { - roots.Add (dir, d); - } + roots.TryAdd (dir, d); } } catch (Exception) @@ -181,7 +178,7 @@ private Dictionary DefaultTreeRootGetter () IDirectoryInfo dir = _fileSystem.DirectoryInfo.New (path); - if (!roots.ContainsKey (dir) && dir.Exists) + if (!roots.ContainsKey (dir) && !roots.ContainsValue (special.ToString ()) && dir.Exists) { roots.Add (dir, special.ToString ()); } diff --git a/Terminal.Gui/Views/FileDialog.cs b/Terminal.Gui/Views/FileDialog.cs index cdeb939a82..2f1aa21abc 100644 --- a/Terminal.Gui/Views/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialog.cs @@ -103,6 +103,8 @@ internal FileDialog (IFileSystem fileSystem) return; } + e.Cancel = true; + if (Modal) { Application.RequestStop (); @@ -111,15 +113,27 @@ internal FileDialog (IFileSystem fileSystem) _btnUp = new() { X = 0, Y = 1, NoPadding = true }; _btnUp.Text = GetUpButtonText (); - _btnUp.Accepting += (s, e) => _history.Up (); + _btnUp.Accepting += (s, e) => + { + _history.Up (); + e.Cancel = true; + }; _btnBack = new() { X = Pos.Right (_btnUp) + 1, Y = 1, NoPadding = true }; _btnBack.Text = GetBackButtonText (); - _btnBack.Accepting += (s, e) => _history.Back (); + _btnBack.Accepting += (s, e) => + { + _history.Back (); + e.Cancel = true; + }; _btnForward = new() { X = Pos.Right (_btnBack) + 1, Y = 1, NoPadding = true }; _btnForward.Text = GetForwardButtonText (); - _btnForward.Accepting += (s, e) => _history.Forward (); + _btnForward.Accepting += (s, e) => + { + _history.Forward(); + e.Cancel = true; + }; _tbPath = new() { Width = Dim.Fill (), CaptionColor = new (Color.Black) }; @@ -199,6 +213,8 @@ internal FileDialog (IFileSystem fileSystem) _btnToggleSplitterCollapse.Accepting += (s, e) => { + // Required otherwise the Save button clicks itself + e.Cancel = true; Tile tile = _splitContainer.Tiles.ElementAt (0); bool newState = !tile.ContentView.Visible; @@ -490,7 +506,7 @@ public override void OnLoaded () // if no path has been provided if (_tbPath.Text.Length <= 0) { - Path = Environment.CurrentDirectory; + Path = _fileSystem.Directory.GetCurrentDirectory (); } // to streamline user experience and allow direct typing of paths @@ -1288,7 +1304,7 @@ private IDirectoryInfo StringToDirectoryInfo (string path) // really not what most users would expect if (Regex.IsMatch (path, "^\\w:$")) { - return _fileSystem.DirectoryInfo.New (path + System.IO.Path.DirectorySeparatorChar); + return _fileSystem.DirectoryInfo.New (path + _fileSystem.Path.DirectorySeparatorChar); } return _fileSystem.DirectoryInfo.New (path); diff --git a/Terminal.Gui/Views/SaveDialog.cs b/Terminal.Gui/Views/SaveDialog.cs index 61ffb88e47..3c7f52be59 100644 --- a/Terminal.Gui/Views/SaveDialog.cs +++ b/Terminal.Gui/Views/SaveDialog.cs @@ -9,6 +9,7 @@ // * Use a line separator to show the file listing, so we can use same colors as the rest // * DirListView: Add mouse support +using System.IO.Abstractions; using Terminal.Gui.Resources; namespace Terminal.Gui; @@ -24,8 +25,15 @@ namespace Terminal.Gui; public class SaveDialog : FileDialog { /// Initializes a new . - public SaveDialog () { Style.OkButtonText = Strings.btnSave; } + public SaveDialog () + { + Style.OkButtonText = Strings.btnSave; + } + internal SaveDialog (IFileSystem fileSystem) : base (fileSystem) + { + Style.OkButtonText = Strings.btnSave; + } /// /// Gets the name of the file the user selected for saving, or null if the user canceled the /// . diff --git a/Terminal.sln b/Terminal.sln index b255d94598..1456297b83 100644 --- a/Terminal.sln +++ b/Terminal.sln @@ -65,7 +65,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests.Parallelizable", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTesting", "TerminalGuiFluentTesting\TerminalGuiFluentTesting.csproj", "{2DBA7BDC-17AE-474B-A507-00807D087607}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTesting.Xunit", "TerminalGuiFluentTesting.Xunit\TerminalGuiFluentTesting.Xunit.csproj", "{231B9723-10F3-46DB-8EAE-50C0C0375AD3}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTestingXunit", "TerminalGuiFluentTestingXunit\TerminalGuiFluentTestingXunit.csproj", "{F56BAFFD-F227-4B0A-96F0-C800FAEF2036}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerminalGuiFluentTestingXunit.Generator", "TerminalGuiFluentTestingXunit.Generator\TerminalGuiFluentTestingXunit.Generator.csproj", "{199F27D8-A905-4DDC-82CA-1FE1A90B1788}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -125,10 +127,14 @@ Global {2DBA7BDC-17AE-474B-A507-00807D087607}.Debug|Any CPU.Build.0 = Debug|Any CPU {2DBA7BDC-17AE-474B-A507-00807D087607}.Release|Any CPU.ActiveCfg = Release|Any CPU {2DBA7BDC-17AE-474B-A507-00807D087607}.Release|Any CPU.Build.0 = Release|Any CPU - {231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {231B9723-10F3-46DB-8EAE-50C0C0375AD3}.Release|Any CPU.Build.0 = Release|Any CPU + {F56BAFFD-F227-4B0A-96F0-C800FAEF2036}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F56BAFFD-F227-4B0A-96F0-C800FAEF2036}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F56BAFFD-F227-4B0A-96F0-C800FAEF2036}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F56BAFFD-F227-4B0A-96F0-C800FAEF2036}.Release|Any CPU.Build.0 = Release|Any CPU + {199F27D8-A905-4DDC-82CA-1FE1A90B1788}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {199F27D8-A905-4DDC-82CA-1FE1A90B1788}.Debug|Any CPU.Build.0 = Debug|Any CPU + {199F27D8-A905-4DDC-82CA-1FE1A90B1788}.Release|Any CPU.ActiveCfg = Release|Any CPU + {199F27D8-A905-4DDC-82CA-1FE1A90B1788}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/TerminalGuiFluentTesting/GuiTestContext.cs b/TerminalGuiFluentTesting/GuiTestContext.cs index 1a3274cd32..9b9fe7af4d 100644 --- a/TerminalGuiFluentTesting/GuiTestContext.cs +++ b/TerminalGuiFluentTesting/GuiTestContext.cs @@ -1,7 +1,9 @@ -using System.Text; +using System.Drawing; +using System.Text; using Microsoft.Extensions.Logging; using Terminal.Gui; using Terminal.Gui.ConsoleDrivers; +using static Unix.Terminal.Curses; namespace TerminalGuiFluentTesting; @@ -21,6 +23,7 @@ public class GuiTestContext : IDisposable private View? _lastView; private readonly StringBuilder _logsSb; private readonly V2TestDriver _driver; + private bool _finished=false; internal GuiTestContext (Func topLevelBuilder, int width, int height, V2TestDriver driver) { @@ -62,7 +65,7 @@ internal GuiTestContext (Func topLevelBuilder, int width, int height, booting.Release (); Toplevel t = topLevelBuilder (); - + t.Closed += (s, e) => { _finished = true; }; Application.Run (t); // This will block, but it's on a background thread now Application.Shutdown (); @@ -77,6 +80,7 @@ internal GuiTestContext (Func topLevelBuilder, int width, int height, { ApplicationImpl.ChangeInstance (origApp); Logging.Logger = origLogger; + _finished = true; } }, _cts.Token); @@ -111,7 +115,7 @@ public GuiTestContext Stop () return this; } - Application.Invoke (() => Application.RequestStop ()); + Application.Invoke (() => {Application.RequestStop ();}); // Wait for the application to stop, but give it a 1-second timeout if (!_runTask.Wait (TimeSpan.FromMilliseconds (1000))) @@ -134,6 +138,15 @@ public GuiTestContext Stop () return this; } + /// + /// Hard stops the application and waits for the background thread to exit. + /// + public void HardStop () + { + _hardStop.Cancel (); + Stop (); + } + /// /// Cleanup to avoid state bleed between tests /// @@ -213,6 +226,12 @@ public GuiTestContext WriteOutLogs (TextWriter writer) /// public GuiTestContext WaitIteration (Action? a = null) { + // If application has already exited don't wait! + if (_finished || _cts.Token.IsCancellationRequested || _hardStop.Token.IsCancellationRequested) + { + return this; + } + a ??= () => { }; var ctsLocal = new CancellationTokenSource (); @@ -249,8 +268,7 @@ public GuiTestContext Then (Action doAction) } catch(Exception) { - Stop (); - _hardStop.Cancel(); + HardStop (); throw; @@ -259,6 +277,7 @@ public GuiTestContext Then (Action doAction) return this; } + /// /// Simulates a right click at the given screen coordinates on the current driver. /// This is a raw input event that goes through entire processing pipeline as though @@ -277,8 +296,22 @@ public GuiTestContext Then (Action doAction) /// 0 indexed screen coordinates /// 0 indexed screen coordinates /// - public GuiTestContext LeftClick (int screenX, int screenY) { return Click (WindowsConsole.ButtonState.Button1Pressed, screenX, screenY); } + public GuiTestContext LeftClick (int screenX, int screenY) + { + return Click (WindowsConsole.ButtonState.Button1Pressed, screenX, screenY); + } + public GuiTestContext LeftClick (Func evaluator) where T : View + { + return Click (WindowsConsole.ButtonState.Button1Pressed,evaluator); + } + + private GuiTestContext Click (WindowsConsole.ButtonState btn, Func evaluator) where T:View + { + var v = Find (evaluator); + var screen = v.ViewportToScreen (new Point (0, 0)); + return Click (btn, screen.X, screen.Y); + } private GuiTestContext Click (WindowsConsole.ButtonState btn, int screenX, int screenY) { switch (_driver) @@ -462,6 +495,75 @@ public GuiTestContext Enter () return this; } + + /// + /// Simulates pressing the Esc (Escape) key. + /// + /// + /// + public GuiTestContext Escape () + { + switch (_driver) + { + case V2TestDriver.V2Win: + SendWindowsKey ( + new WindowsConsole.KeyEventRecord + { + UnicodeChar = '\u001b', + dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed, + wRepeatCount = 1, + wVirtualKeyCode = ConsoleKeyMapping.VK.ESCAPE, + wVirtualScanCode = 1 + }); + break; + case V2TestDriver.V2Net: + + // Note that this accurately describes how Esc comes in. Typically, ConsoleKey is None + // even though you would think it would be Escape - it isn't + SendNetKey (new ('\u001b', ConsoleKey.None, false, false, false)); + break; + default: + throw new ArgumentOutOfRangeException (); + } + + return this; + } + + + + /// + /// Simulates pressing the Tab key. + /// + /// + /// + public GuiTestContext Tab () + { + switch (_driver) + { + case V2TestDriver.V2Win: + SendWindowsKey ( + new WindowsConsole.KeyEventRecord + { + UnicodeChar = '\t', + dwControlKeyState = WindowsConsole.ControlKeyState.NoControlKeyPressed, + wRepeatCount = 1, + wVirtualKeyCode = 0, + wVirtualScanCode = 0 + }); + break; + case V2TestDriver.V2Net: + + // Note that this accurately describes how Tab comes in. Typically, ConsoleKey is None + // even though you would think it would be Tab - it isn't + SendNetKey (new ('\t', ConsoleKey.None, false, false, false)); + break; + default: + throw new ArgumentOutOfRangeException (); + } + + return this; + } + /// /// Registers a right click handler on the added view (or root view) that /// will open the supplied . @@ -583,4 +685,119 @@ public GuiTestContext Focus (View toFocus) return WaitIteration (); } + + /// + /// Tabs through the UI until a View matching the + /// is found (of Type T) or all views are looped through (back to the beginning) + /// in which case triggers hard stop and Exception + /// + /// + /// + public GuiTestContext Focus (Func evaluator) where T:View + { + var t = Application.Top; + + HashSet seen = new (); + + if (t == null) + { + Fail ("Application.Top was null when trying to set focus"); + return this; + } + + do + { + var next = t.MostFocused; + + // Is view found? + if (next is T v && evaluator (v)) + { + return this; + } + + // No, try tab to the next (or first) + this.Tab (); + WaitIteration (); + next = t.MostFocused; + + if (next is null) + { + Fail ("Failed to tab to a view which matched the Type and evaluator constraints of the test because MostFocused became or was always null"); + return this; + } + + // Track the views we have seen + // We have looped around to the start again if it was already there + if (!seen.Add (next)) + { + Fail ("Failed to tab to a view which matched the Type and evaluator constraints of the test before looping back to the original View"); + + return this; + } + + } + while (true); + } + + + + private T Find (Func evaluator) where T : View + { + var t = Application.Top; + + if (t == null) + { + Fail ("Application.Top was null when attempting to find view"); + } + var f = FindRecursive(t!, evaluator); + + if (f == null) + { + Fail ("Failed to tab to a view which matched the Type and evaluator constraints in any SubViews of top"); + } + + return f!; + } + + private T? FindRecursive (View current, Func evaluator) where T : View + { + foreach (var subview in current.SubViews) + { + if (subview is T match && evaluator (match)) + { + return match; + } + + // Recursive call + var result = FindRecursive (subview, evaluator); + if (result != null) + { + return result; + } + } + + return null; + } + + private void Fail (string reason) + { + Stop (); + + throw new Exception (reason); + + } + + public GuiTestContext Send (Key key) + { + if (Application.Driver is IConsoleDriverFacade facade) + { + facade.InputProcessor.OnKeyDown (key); + facade.InputProcessor.OnKeyUp (key); + } + else + { + Fail ("Expected Application.Driver to be IConsoleDriverFacade"); + } + return this; + } } diff --git a/TerminalGuiFluentTesting/With.cs b/TerminalGuiFluentTesting/With.cs index b65d832385..078fdb1896 100644 --- a/TerminalGuiFluentTesting/With.cs +++ b/TerminalGuiFluentTesting/With.cs @@ -19,8 +19,23 @@ public static class With return new (() => new T (), width, height,v2TestDriver); } + /// + /// Overload that takes an existing instance + /// instead of creating one. + /// + /// + /// + /// + /// + /// + public static GuiTestContext A (Toplevel toplevel, int width, int height, V2TestDriver v2TestDriver) + { + return new (()=>toplevel, width, height, v2TestDriver); + } /// /// The global timeout to allow for any given application to run for before shutting down. /// public static TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds (30); + + } diff --git a/TerminalGuiFluentTestingXunit.Generator/TerminalGuiFluentTestingXunit.Generator.csproj b/TerminalGuiFluentTestingXunit.Generator/TerminalGuiFluentTestingXunit.Generator.csproj new file mode 100644 index 0000000000..454cc7bf7f --- /dev/null +++ b/TerminalGuiFluentTestingXunit.Generator/TerminalGuiFluentTestingXunit.Generator.csproj @@ -0,0 +1,20 @@ + + + + + netstandard2.0 + Latest + enable + enable + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs b/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs new file mode 100644 index 0000000000..5c0427d115 --- /dev/null +++ b/TerminalGuiFluentTestingXunit.Generator/TheGenerator.cs @@ -0,0 +1,333 @@ +using System.Collections.Immutable; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace TerminalGuiFluentTestingXunit.Generator; + +[Generator] +public class TheGenerator : IIncrementalGenerator +{ + /// + public void Initialize (IncrementalGeneratorInitializationContext context) + { + IncrementalValuesProvider provider = context.SyntaxProvider.CreateSyntaxProvider ( + static (node, _) => IsClass (node, "XunitContextExtensions"), + static (ctx, _) => + (ClassDeclarationSyntax)ctx.Node) + .Where (m => m is { }); + + IncrementalValueProvider<(Compilation Left, ImmutableArray Right)> compilation = + context.CompilationProvider.Combine (provider.Collect ()); + context.RegisterSourceOutput (compilation, Execute); + } + + private static bool IsClass (SyntaxNode node, string named) { return node is ClassDeclarationSyntax c && c.Identifier.Text == named; } + + private void Execute (SourceProductionContext context, (Compilation Left, ImmutableArray Right) arg2) + { + INamedTypeSymbol assertType = arg2.Left.GetTypeByMetadataName ("Xunit.Assert") + ?? throw new NotSupportedException("Referencing codebase does not include Xunit, could not find Xunit.Assert"); + + GenerateMethods (assertType, context, "Equal", false); + + GenerateMethods (assertType, context, "All", true); + GenerateMethods (assertType, context, "Collection", true); + GenerateMethods (assertType, context, "Contains", true); + GenerateMethods (assertType, context, "Distinct", true); + GenerateMethods (assertType, context, "DoesNotContain", true); + GenerateMethods (assertType, context, "DoesNotMatch", true); + GenerateMethods (assertType, context, "Empty", true); + GenerateMethods (assertType, context, "EndsWith", false); + GenerateMethods (assertType, context, "Equivalent", true); + GenerateMethods (assertType, context, "Fail", true); + GenerateMethods (assertType, context, "False", true); + GenerateMethods (assertType, context, "InRange", true); + GenerateMethods (assertType, context, "IsAssignableFrom", true); + GenerateMethods (assertType, context, "IsNotAssignableFrom", true); + GenerateMethods (assertType, context, "IsType", true); + GenerateMethods (assertType, context, "IsNotType", true); + + GenerateMethods (assertType, context, "Matches", true); + GenerateMethods (assertType, context, "Multiple", true); + GenerateMethods (assertType, context, "NotEmpty", true); + GenerateMethods (assertType, context, "NotEqual", true); + GenerateMethods (assertType, context, "NotInRange", true); + GenerateMethods (assertType, context, "NotNull", false); + GenerateMethods (assertType, context, "NotSame", true); + GenerateMethods (assertType, context, "NotStrictEqual", true); + GenerateMethods (assertType, context, "Null", false); + GenerateMethods (assertType, context, "ProperSubset", true); + GenerateMethods (assertType, context, "ProperSuperset", true); + GenerateMethods (assertType, context, "Raises", true); + GenerateMethods (assertType, context, "RaisesAny", true); + GenerateMethods (assertType, context, "Same", true); + GenerateMethods (assertType, context, "Single", true); + GenerateMethods (assertType, context, "StartsWith", false); + + GenerateMethods (assertType, context, "StrictEqual", true); + GenerateMethods (assertType, context, "Subset", true); + GenerateMethods (assertType, context, "Superset", true); + +// GenerateMethods (assertType, context, "Throws", true); + // GenerateMethods (assertType, context, "ThrowsAny", true); + GenerateMethods (assertType, context, "True", false); + } + + private void GenerateMethods (INamedTypeSymbol assertType, SourceProductionContext context, string methodName, bool invokeTExplicitly) + { + var sb = new StringBuilder (); + + // Create a HashSet to track unique method signatures + HashSet signaturesDone = new (); + + List methods = assertType + .GetMembers (methodName) + .OfType () + .ToList (); + + var header = """" + #nullable enable + using TerminalGuiFluentTesting; + using Xunit; + + namespace TerminalGuiFluentTestingXunit; + + public static partial class XunitContextExtensions + { + + + """"; + + var tail = """ + + } + """; + + sb.AppendLine (header); + + foreach (IMethodSymbol? m in methods) + { + string signature = GetModifiedMethodSignature (m, methodName, invokeTExplicitly, out string [] paramNames, out string typeParams); + + if (!signaturesDone.Add (signature)) + { + continue; + } + + var method = $$""" + {{signature}} + { + try + { + Assert.{{methodName}}{{typeParams}} ({{string.Join (",", paramNames)}}); + } + catch(Exception) + { + context.HardStop (); + + + throw; + + } + + return context; + } + """; + + sb.AppendLine (method); + } + + sb.AppendLine (tail); + + context.AddSource ($"XunitContextExtensions{methodName}.g.cs", sb.ToString ()); + } + + private string GetModifiedMethodSignature ( + IMethodSymbol methodSymbol, + string methodName, + bool invokeTExplicitly, + out string [] paramNames, + out string typeParams + ) + { + typeParams = string.Empty; + + // Create the "this GuiTestContext context" parameter + ParameterSyntax contextParam = SyntaxFactory.Parameter (SyntaxFactory.Identifier ("context")) + .WithType (SyntaxFactory.ParseTypeName ("GuiTestContext")) + .AddModifiers (SyntaxFactory.Token (SyntaxKind.ThisKeyword)); // Add the "this" keyword + + // Extract the parameter names (expected and actual) + paramNames = new string [methodSymbol.Parameters.Length]; + + for (var i = 0; i < methodSymbol.Parameters.Length; i++) + { + paramNames [i] = methodSymbol.Parameters.ElementAt (i).Name; + + // Check if the parameter name is a reserved keyword and prepend "@" if it is + if (IsReservedKeyword (paramNames [i])) + { + paramNames [i] = "@" + paramNames [i]; + } + else + { + paramNames [i] = paramNames [i]; + } + } + + // Get the current method parameters and add the context parameter at the start + List parameters = methodSymbol.Parameters.Select (p => CreateParameter (p)).ToList (); + + parameters.Insert (0, contextParam); // Insert 'context' as the first parameter + + // Change the return type to GuiTestContext + TypeSyntax returnType = SyntaxFactory.ParseTypeName ("GuiTestContext"); + + // Change the method name to AssertEqual + SyntaxToken newMethodName = SyntaxFactory.Identifier ($"Assert{methodName}"); + + // Handle generic type parameters if the method is generic + TypeParameterSyntax [] typeParameters = methodSymbol.TypeParameters.Select ( + tp => + SyntaxFactory.TypeParameter (SyntaxFactory.Identifier (tp.Name)) + ) + .ToArray (); + + MethodDeclarationSyntax dec = SyntaxFactory.MethodDeclaration (returnType, newMethodName) + .WithModifiers ( + SyntaxFactory.TokenList ( + SyntaxFactory.Token (SyntaxKind.PublicKeyword), + SyntaxFactory.Token (SyntaxKind.StaticKeyword))) + .WithParameterList (SyntaxFactory.ParameterList (SyntaxFactory.SeparatedList (parameters))); + + if (typeParameters.Any ()) + { + // Add the here + dec = dec.WithTypeParameterList (SyntaxFactory.TypeParameterList (SyntaxFactory.SeparatedList (typeParameters))); + + // Handle type parameter constraints + List constraintClauses = methodSymbol.TypeParameters + .Where (tp => tp.ConstraintTypes.Length > 0) + .Select ( + tp => + SyntaxFactory.TypeParameterConstraintClause (tp.Name) + .WithConstraints ( + SyntaxFactory + .SeparatedList ( + tp.ConstraintTypes.Select ( + constraintType => + SyntaxFactory.TypeConstraint ( + SyntaxFactory.ParseTypeName ( + constraintType + .ToDisplayString ())) + ) + ) + ) + ) + .ToList (); + + if (constraintClauses.Any ()) + { + dec = dec.WithConstraintClauses (SyntaxFactory.List (constraintClauses)); + } + + // Add the here + if (invokeTExplicitly) + { + typeParams = "<" + string.Join (", ", typeParameters.Select (tp => tp.Identifier.ValueText)) + ">"; + } + } + + // Build the method signature syntax tree + MethodDeclarationSyntax methodSyntax = dec.NormalizeWhitespace (); + + // Convert the method syntax to a string + var methodString = methodSyntax.ToString (); + + return methodString; + } + + /// + /// Creates a from a discovered parameter on real xunit method parameter + /// + /// + /// + /// + private ParameterSyntax CreateParameter (IParameterSymbol p) + { + string paramName = p.Name; + + // Check if the parameter name is a reserved keyword and prepend "@" if it is + if (IsReservedKeyword (paramName)) + { + paramName = "@" + paramName; + } + + // Create the basic parameter syntax with the modified name and type + ParameterSyntax parameterSyntax = SyntaxFactory.Parameter (SyntaxFactory.Identifier (paramName)) + .WithType (SyntaxFactory.ParseTypeName (p.Type.ToDisplayString ())); + + // Add 'params' keyword if the parameter has the Params modifier + var modifiers = new List (); + + if (p.IsParams) + { + modifiers.Add (SyntaxFactory.Token (SyntaxKind.ParamsKeyword)); + } + + // Handle ref/out/in modifiers + if (p.RefKind != RefKind.None) + { + SyntaxKind modifierKind = p.RefKind switch + { + RefKind.Ref => SyntaxKind.RefKeyword, + RefKind.Out => SyntaxKind.OutKeyword, + RefKind.In => SyntaxKind.InKeyword, + _ => throw new NotSupportedException ($"Unsupported RefKind: {p.RefKind}") + }; + + + modifiers.Add (SyntaxFactory.Token (modifierKind)); + } + + + if (modifiers.Any ()) + { + parameterSyntax = parameterSyntax.WithModifiers (SyntaxFactory.TokenList (modifiers)); + } + + // Add default value if one is present + if (p.HasExplicitDefaultValue) + { + ExpressionSyntax defaultValueExpression = p.ExplicitDefaultValue switch + { + null => SyntaxFactory.LiteralExpression (SyntaxKind.NullLiteralExpression), + bool b => SyntaxFactory.LiteralExpression ( + b + ? SyntaxKind.TrueLiteralExpression + : SyntaxKind.FalseLiteralExpression), + int i => SyntaxFactory.LiteralExpression ( + SyntaxKind.NumericLiteralExpression, + SyntaxFactory.Literal (i)), + double d => SyntaxFactory.LiteralExpression ( + SyntaxKind.NumericLiteralExpression, + SyntaxFactory.Literal (d)), + string s => SyntaxFactory.LiteralExpression ( + SyntaxKind.StringLiteralExpression, + SyntaxFactory.Literal (s)), + _ => SyntaxFactory.ParseExpression (p.ExplicitDefaultValue.ToString ()) // Fallback + }; + + parameterSyntax = parameterSyntax.WithDefault ( + SyntaxFactory.EqualsValueClause (defaultValueExpression) + ); + } + + return parameterSyntax; + } + + // Helper method to check if a parameter name is a reserved keyword + private bool IsReservedKeyword (string name) { return string.Equals (name, "object"); } +} diff --git a/TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj b/TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj new file mode 100644 index 0000000000..e9e661df26 --- /dev/null +++ b/TerminalGuiFluentTestingXunit/TerminalGuiFluentTestingXunit.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + true + CS8714 + + + + + + + + + diff --git a/TerminalGuiFluentTestingXunit/XunitContextExtensions.cs b/TerminalGuiFluentTestingXunit/XunitContextExtensions.cs new file mode 100644 index 0000000000..a007dbbc1b --- /dev/null +++ b/TerminalGuiFluentTestingXunit/XunitContextExtensions.cs @@ -0,0 +1,9 @@ +using TerminalGuiFluentTesting; +using Xunit; + +namespace TerminalGuiFluentTestingXunit; + +public static partial class XunitContextExtensions +{ + // Placeholder +} diff --git a/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs new file mode 100644 index 0000000000..2eeb59484e --- /dev/null +++ b/Tests/IntegrationTests/FluentTests/FileDialogFluentTests.cs @@ -0,0 +1,197 @@ +using System.IO.Abstractions; +using System.IO.Abstractions.TestingHelpers; +using System.Runtime.InteropServices; +using Terminal.Gui; +using TerminalGuiFluentTesting; +using TerminalGuiFluentTestingXunit; +using Xunit.Abstractions; + +namespace IntegrationTests.FluentTests; +public class FileDialogFluentTests +{ + private readonly TextWriter _out; + + public FileDialogFluentTests (ITestOutputHelper outputHelper) { _out = new TestOutputWriter (outputHelper); } + + private MockFileSystem CreateExampleFileSystem () + { + + // Optional: use Ordinal to simulate Linux-style case sensitivity + var mockFileSystem = new MockFileSystem (new Dictionary ()); + + string testDir = mockFileSystem.Path.Combine ("test-dir"); + string subDir = mockFileSystem.Path.Combine (testDir, "sub-dir"); + string logsDir = "logs"; + string emptyDir = "empty-dir"; + + // Add files + mockFileSystem.AddFile (mockFileSystem.Path.Combine (testDir, "file1.txt"), new MockFileData ("Hello, this is file 1.")); + mockFileSystem.AddFile (mockFileSystem.Path.Combine (testDir, "file2.txt"), new MockFileData ("Hello, this is file 2.")); + mockFileSystem.AddFile (mockFileSystem.Path.Combine (subDir, "nested-file.txt"), new MockFileData ("This is a nested file.")); + mockFileSystem.AddFile (mockFileSystem.Path.Combine (logsDir, "log1.log"), new MockFileData ("Log entry 1")); + mockFileSystem.AddFile (mockFileSystem.Path.Combine (logsDir, "log2.log"), new MockFileData ("Log entry 2")); + + // Create an empty directory + mockFileSystem.AddDirectory (emptyDir); + + return mockFileSystem; + } + + [Theory] + [ClassData (typeof (V2TestDrivers))] + public void CancelFileDialog_UsingEscape (V2TestDriver d) + { + var sd = new SaveDialog ( CreateExampleFileSystem ()); + using var c = With.A (sd, 100, 20, d) + .ScreenShot ("Save dialog",_out) + .Escape() + .Stop (); + + Assert.True (sd.Canceled); + } + + [Theory] + [ClassData (typeof (V2TestDrivers))] + public void CancelFileDialog_UsingCancelButton_TabThenEnter (V2TestDriver d) + { + var sd = new SaveDialog (CreateExampleFileSystem ()); + using var c = With.A (sd, 100, 20, d) + .ScreenShot ("Save dialog", _out) + .Focus