diff --git a/Terminal.Gui/Configuration/AttributeJsonConverter.cs b/Terminal.Gui/Configuration/AttributeJsonConverter.cs index 66d085baf3..55bd84575f 100644 --- a/Terminal.Gui/Configuration/AttributeJsonConverter.cs +++ b/Terminal.Gui/Configuration/AttributeJsonConverter.cs @@ -1,99 +1,97 @@ using System; using System.Text.Json; using System.Text.Json.Serialization; -using Terminal.Gui; -namespace Terminal.Gui { - /// - /// Json converter fro the class. - /// - class AttributeJsonConverter : JsonConverter { - private static AttributeJsonConverter instance; +namespace Terminal.Gui; - /// - /// - /// - public static AttributeJsonConverter Instance { - get { - if (instance == null) { - instance = new AttributeJsonConverter (); - } +/// +/// Json converter fro the class. +/// +class AttributeJsonConverter : JsonConverter { + static AttributeJsonConverter _instance; - return instance; + /// + /// + /// + public static AttributeJsonConverter Instance { + get { + if (_instance == null) { + _instance = new AttributeJsonConverter (); } + + return _instance; } + } - public override Attribute Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType != JsonTokenType.StartObject) { - throw new JsonException ($"Unexpected StartObject token when parsing Attribute: {reader.TokenType}."); - } + public override Attribute Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) { + throw new JsonException ($"Unexpected StartObject token when parsing Attribute: {reader.TokenType}."); + } - Attribute attribute = new Attribute (); - Color foreground = null; - Color background = null; - while (reader.Read ()) { - if (reader.TokenType == JsonTokenType.EndObject) { - if (foreground == null || background == null) { - throw new JsonException ($"Both Foreground and Background colors must be provided."); - } - return new Attribute (foreground, background); + var attribute = new Attribute (); + Color? foreground = null; + Color? background = null; + while (reader.Read ()) { + if (reader.TokenType == JsonTokenType.EndObject) { + if (foreground == null || background == null) { + throw new JsonException ("Both Foreground and Background colors must be provided."); } + return new Attribute (foreground.Value, background.Value); + } - if (reader.TokenType != JsonTokenType.PropertyName) { - throw new JsonException ($"Unexpected token when parsing Attribute: {reader.TokenType}."); - } + if (reader.TokenType != JsonTokenType.PropertyName) { + throw new JsonException ($"Unexpected token when parsing Attribute: {reader.TokenType}."); + } - string propertyName = reader.GetString (); - reader.Read (); - string color = $"\"{reader.GetString ()}\""; + var propertyName = reader.GetString (); + reader.Read (); + var color = $"\"{reader.GetString ()}\""; - switch (propertyName.ToLower ()) { - case "foreground": - foreground = JsonSerializer.Deserialize (color, options); - break; - case "background": - background = JsonSerializer.Deserialize (color, options); - break; - //case "bright": - //case "bold": - // attribute.Bright = reader.GetBoolean (); - // break; - //case "dim": - // attribute.Dim = reader.GetBoolean (); - // break; - //case "underline": - // attribute.Underline = reader.GetBoolean (); - // break; - //case "blink": - // attribute.Blink = reader.GetBoolean (); - // break; - //case "reverse": - // attribute.Reverse = reader.GetBoolean (); - // break; - //case "hidden": - // attribute.Hidden = reader.GetBoolean (); - // break; - //case "strike-through": - // attribute.StrikeThrough = reader.GetBoolean (); - // break; - default: - throw new JsonException ($"Unknown Attribute property {propertyName}."); - } + switch (propertyName?.ToLower ()) { + case "foreground": + foreground = JsonSerializer.Deserialize (color, options); + break; + case "background": + background = JsonSerializer.Deserialize (color, options); + break; + //case "bright": + //case "bold": + // attribute.Bright = reader.GetBoolean (); + // break; + //case "dim": + // attribute.Dim = reader.GetBoolean (); + // break; + //case "underline": + // attribute.Underline = reader.GetBoolean (); + // break; + //case "blink": + // attribute.Blink = reader.GetBoolean (); + // break; + //case "reverse": + // attribute.Reverse = reader.GetBoolean (); + // break; + //case "hidden": + // attribute.Hidden = reader.GetBoolean (); + // break; + //case "strike-through": + // attribute.StrikeThrough = reader.GetBoolean (); + // break; + default: + throw new JsonException ($"Unknown Attribute property {propertyName}."); } - throw new JsonException (); - } - - public override void Write (Utf8JsonWriter writer, Attribute value, JsonSerializerOptions options) - { - writer.WriteStartObject (); - writer.WritePropertyName (nameof(Attribute.Foreground)); - ColorJsonConverter.Instance.Write (writer, value.Foreground, options); - writer.WritePropertyName (nameof (Attribute.Background)); - ColorJsonConverter.Instance.Write (writer, value.Background, options); - - writer.WriteEndObject (); } + throw new JsonException (); } -} + public override void Write (Utf8JsonWriter writer, Attribute value, JsonSerializerOptions options) + { + writer.WriteStartObject (); + writer.WritePropertyName (nameof (Attribute.Foreground)); + ColorJsonConverter.Instance.Write (writer, value.Foreground, options); + writer.WritePropertyName (nameof (Attribute.Background)); + ColorJsonConverter.Instance.Write (writer, value.Background, options); + + writer.WriteEndObject (); + } +} \ No newline at end of file diff --git a/Terminal.Gui/Configuration/ConfigProperty.cs b/Terminal.Gui/Configuration/ConfigProperty.cs index 64b59a43ed..7ce4e77371 100644 --- a/Terminal.Gui/Configuration/ConfigProperty.cs +++ b/Terminal.Gui/Configuration/ConfigProperty.cs @@ -1,31 +1,41 @@ -using System; +#nullable enable + +using System; using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; -#nullable enable - namespace Terminal.Gui; /// -/// Holds a property's value and the that allows +/// Holds a property's value and the that allows /// to get and set the property's value. /// /// -/// Configuration properties must be and +/// Configuration properties must be and /// and have the -/// attribute. If the type of the property requires specialized JSON serialization, -/// a must be provided using +/// attribute. If the type of the property requires specialized JSON serialization, +/// a must be provided using /// the attribute. /// public class ConfigProperty { - private object? propertyValue; /// /// Describes the property. /// public PropertyInfo? PropertyInfo { get; set; } + /// + /// Holds the property's value as it was either read from the class's implementation or from a config file. + /// If the property has not been set (e.g. because no configuration file specified a value), + /// this will be . + /// + /// + /// On , performs a sparse-copy of the new value to the existing value (only copies elements of + /// the object that are non-null). + /// + public object? PropertyValue { get; set; } + /// /// Helper to get either the Json property named (specified by [JsonPropertyName(name)] /// or the actual property name. @@ -38,22 +48,6 @@ public static string GetJsonPropertyName (PropertyInfo pi) return jpna?.Name ?? pi.Name; } - /// - /// Holds the property's value as it was either read from the class's implementation or from a config file. - /// If the property has not been set (e.g. because no configuration file specified a value), - /// this will be . - /// - /// - /// On , performs a sparse-copy of the new value to the existing value (only copies elements of - /// the object that are non-null). - /// - public object? PropertyValue { - get => propertyValue; - set { - propertyValue = value; - } - } - internal object? UpdateValueFrom (object source) { if (source == null) { @@ -61,11 +55,11 @@ public object? PropertyValue { } var ut = Nullable.GetUnderlyingType (PropertyInfo!.PropertyType); - if (source.GetType () != PropertyInfo!.PropertyType && (ut != null && source.GetType () != ut)) { + if (source.GetType () != PropertyInfo!.PropertyType && ut != null && source.GetType () != ut) { throw new ArgumentException ($"The source object ({PropertyInfo!.DeclaringType}.{PropertyInfo!.Name}) is not of type {PropertyInfo!.PropertyType}."); } - if (PropertyValue != null && source != null) { - PropertyValue = ConfigurationManager.DeepMemberwiseCopy (source, PropertyValue); + if (PropertyValue != null) { + PropertyValue = DeepMemberwiseCopy (source, PropertyValue); } else { PropertyValue = source; } @@ -78,10 +72,7 @@ public object? PropertyValue { /// into . /// /// - public object? RetrieveValue () - { - return PropertyValue = PropertyInfo!.GetValue (null); - } + public object? RetrieveValue () => PropertyValue = PropertyInfo!.GetValue (null); /// /// Applies the to the property described by . @@ -91,12 +82,12 @@ public bool Apply () { if (PropertyValue != null) { try { - PropertyInfo?.SetValue (null, ConfigurationManager.DeepMemberwiseCopy (PropertyValue, PropertyInfo?.GetValue (null))); + PropertyInfo?.SetValue (null, DeepMemberwiseCopy (PropertyValue, PropertyInfo?.GetValue (null))); } catch (TargetInvocationException tie) { // Check if there is an inner exception if (tie.InnerException != null) { // Handle the inner exception separately without catching the outer exception - Exception innerException = tie.InnerException; + var innerException = tie.InnerException; // Handle the inner exception here throw new JsonException ($"Error Applying Configuration Change: {innerException.Message}", innerException); @@ -104,9 +95,10 @@ public bool Apply () // Handle the outer exception or rethrow it if needed throw new JsonException ($"Error Applying Configuration Change: {tie.Message}", tie); + } catch (ArgumentException ae) { + throw new JsonException ($"Error Applying Configuration Change ({PropertyInfo?.Name}): {ae.Message}", ae); } } return PropertyValue != null; } - -} +} \ No newline at end of file diff --git a/Terminal.Gui/Configuration/ConfigurationManager.cs b/Terminal.Gui/Configuration/ConfigurationManager.cs index 9510848111..977e81d7c0 100644 --- a/Terminal.Gui/Configuration/ConfigurationManager.cs +++ b/Terminal.Gui/Configuration/ConfigurationManager.cs @@ -1,6 +1,5 @@ global using static Terminal.Gui.ConfigurationManager; global using CM = Terminal.Gui.ConfigurationManager; - using System; using System.Collections; using System.Collections.Generic; @@ -12,7 +11,6 @@ using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; -using static Terminal.Gui.SpinnerStyle; #nullable enable @@ -20,87 +18,124 @@ namespace Terminal.Gui; /// -/// Provides settings and configuration management for Terminal.Gui applications. +/// Provides settings and configuration management for Terminal.Gui applications. /// -/// Users can set Terminal.Gui settings on a global or per-application basis by providing JSON formatted configuration files. -/// The configuration files can be placed in at .tui folder in the user's home directory (e.g. C:/Users/username/.tui, +/// Users can set Terminal.Gui settings on a global or per-application basis by providing JSON formatted configuration +/// files. +/// The configuration files can be placed in at .tui folder in the user's home directory (e.g. +/// C:/Users/username/.tui, /// or /usr/username/.tui), /// the folder where the Terminal.Gui application was launched from (e.g. ./.tui), or as a resource -/// within the Terminal.Gui application's main assembly. +/// within the Terminal.Gui application's main assembly. /// /// -/// Settings are defined in JSON format, according to this schema: -/// https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json +/// Settings are defined in JSON format, according to this schema: +/// https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json /// /// -/// Settings that will apply to all applications (global settings) reside in files named config.json. Settings +/// Settings that will apply to all applications (global settings) reside in files named config.json. Settings /// that will apply to a specific Terminal.Gui application reside in files named appname.config.json, /// where appname is the assembly name of the application (e.g. UICatalog.config.json). /// /// Settings are applied using the following precedence (higher precedence settings /// overwrite lower precedence settings): /// -/// 1. Application configuration found in the users's home directory (~/.tui/appname.config.json) -- Highest precedence +/// 1. Application configuration found in the users's home directory (~/.tui/appname.config.json) -- Highest +/// precedence /// /// -/// 2. Application configuration found in the directory the app was launched from (./.tui/appname.config.json). +/// 2. Application configuration found in the directory the app was launched from (./.tui/appname.config.json). /// /// -/// 3. Application configuration found in the applications's resources (Resources/config.json). +/// 3. Application configuration found in the applications's resources (Resources/config.json). /// /// -/// 4. Global configuration found in the user's home directory (~/.tui/config.json). +/// 4. Global configuration found in the user's home directory (~/.tui/config.json). /// /// -/// 5. Global configuration found in the directory the app was launched from (./.tui/config.json). +/// 5. Global configuration found in the directory the app was launched from (./.tui/config.json). /// /// -/// 6. Global configuration in Terminal.Gui.dll's resources (Terminal.Gui.Resources.config.json) -- Lowest Precidence. +/// 6. Global configuration in Terminal.Gui.dll's resources (Terminal.Gui.Resources.config.json) -- Lowest +/// Precidence. /// /// -public static partial class ConfigurationManager { +public static class ConfigurationManager { + + /// + /// Describes the location of the configuration files. The constants can be + /// combined (bitwise) to specify multiple locations. + /// + [Flags] + public enum ConfigLocations { + /// + /// No configuration will be loaded. + /// + /// + /// Used for development and testing only. For Terminal,Gui to function properly, at least + /// should be set. + /// + None = 0, + + /// + /// Global configuration in Terminal.Gui.dll's resources (Terminal.Gui.Resources.config.json) -- Lowest + /// Precedence. + /// + DefaultOnly, + + /// + /// This constant is a combination of all locations + /// + All = -1 - private static readonly string _configFilename = "config.json"; + } + + static readonly string _configFilename = "config.json"; - internal static readonly JsonSerializerOptions _serializerOptions = new JsonSerializerOptions { + internal static readonly JsonSerializerOptions _serializerOptions = new() { ReadCommentHandling = JsonCommentHandling.Skip, PropertyNameCaseInsensitive = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, WriteIndented = true, Converters = { - // We override the standard Rune converter to support specifying Glyphs in - // a flexible way - new RuneJsonConverter(), - // Override Key to support "Ctrl+Q" format. - new KeyJsonConverter() - }, + // We override the standard Rune converter to support specifying Glyphs in + // a flexible way + new RuneJsonConverter (), + // Override Key to support "Ctrl+Q" format. + new KeyJsonConverter () + }, // Enables Key to be "Ctrl+Q" vs "Ctrl\u002BQ" Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping -}; + }; /// - /// A dictionary of all properties in the Terminal.Gui project that are decorated with the attribute. - /// The keys are the property names pre-pended with the class that implements the property (e.g. Application.UseSystemConsole). + /// A dictionary of all properties in the Terminal.Gui project that are decorated with the + /// attribute. + /// The keys are the property names pre-pended with the class that implements the property (e.g. + /// Application.UseSystemConsole). /// The values are instances of which hold the property's value and the /// that allows to get and set the property's value. /// /// - /// Is until is called. + /// Is until is called. /// internal static Dictionary? _allConfigProperties; /// - /// The backing property for . + /// The backing property for . /// /// /// Is until is called. Gets set to a new instance by /// deserialization (see ). /// - private static SettingsScope? _settings; + static SettingsScope? _settings; + + internal static StringBuilder jsonErrors = new (); /// - /// The root object of Terminal.Gui configuration settings / JSON schema. Contains only properties with the + /// The root object of Terminal.Gui configuration settings / JSON schema. Contains only properties with the + /// /// attribute value. /// public static SettingsScope? Settings { @@ -110,9 +145,7 @@ public static SettingsScope? Settings { } return _settings; } - set { - _settings = value!; - } + set => _settings = value!; } /// @@ -124,15 +157,34 @@ public static SettingsScope? Settings { /// /// Application-specific configuration settings scope. /// - [SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true), JsonPropertyName ("AppSettings")] + [SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)] [JsonPropertyName ("AppSettings")] public static AppScope? AppSettings { get; set; } /// - /// The set of glyphs used to draw checkboxes, lines, borders, etc...See also . + /// The set of glyphs used to draw checkboxes, lines, borders, etc...See also + /// . + /// + [SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true)] [JsonPropertyName ("Glyphs")] + public static GlyphDefinitions Glyphs { get; set; } = new (); + + /// + /// Gets or sets whether the should throw an exception if it encounters + /// an error on deserialization. If (the default), the error is logged and printed to the + /// console when is called. /// - [SerializableConfigurationProperty (Scope = typeof (SettingsScope), OmitClassName = true), - JsonPropertyName ("Glyphs")] - public static GlyphDefinitions Glyphs { get; set; } = new GlyphDefinitions (); + [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] + public static bool? ThrowOnJsonErrors { get; set; } = false; + + /// + /// Name of the running application. By default this property is set to the application's assembly name. + /// + public static string AppName { get; set; } = Assembly.GetEntryAssembly ()?.FullName?.Split (',') [0]?.Trim ()!; + + /// + /// Gets and sets the locations where will look for config files. + /// The value is . + /// + public static ConfigLocations Locations { get; set; } = ConfigLocations.All; /// /// Initializes the internal state of ConfigurationManager. Nominally called once as part of application @@ -143,13 +195,13 @@ internal static void Initialize () _allConfigProperties = new Dictionary (); _settings = null; - Dictionary classesWithConfigProps = new Dictionary (StringComparer.InvariantCultureIgnoreCase); + var classesWithConfigProps = new Dictionary (StringComparer.InvariantCultureIgnoreCase); // Get Terminal.Gui.dll classes var types = from assembly in AppDomain.CurrentDomain.GetAssemblies () - from type in assembly.GetTypes () - where type.GetProperties ().Any (prop => prop.GetCustomAttribute (typeof (SerializableConfigurationProperty)) != null) - select type; + from type in assembly.GetTypes () + where type.GetProperties ().Any (prop => prop.GetCustomAttribute (typeof (SerializableConfigurationProperty)) != null) + select type; foreach (var classWithConfig in types) { classesWithConfigProps.Add (classWithConfig.Name, classWithConfig); @@ -159,11 +211,11 @@ where type.GetProperties ().Any (prop => prop.GetCustomAttribute (typeof (Serial classesWithConfigProps.ToList ().ForEach (x => Debug.WriteLine ($" Class: {x.Key}")); foreach (var p in from c in classesWithConfigProps - let props = c.Value.GetProperties (BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public).Where (prop => - prop.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is SerializableConfigurationProperty) - let enumerable = props - from p in enumerable - select p) { + let props = c.Value.GetProperties (BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public).Where (prop => + prop.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is SerializableConfigurationProperty) + let enumerable = props + from p in enumerable + select p) { if (p.GetCustomAttribute (typeof (SerializableConfigurationProperty)) is SerializableConfigurationProperty scp) { if (p.GetGetMethod (true)!.IsStatic) { // If the class name is omitted, JsonPropertyName is allowed. @@ -186,18 +238,18 @@ from p in enumerable } /// - /// Creates a JSON document with the configuration specified. + /// Creates a JSON document with the configuration specified. /// /// internal static string ToJson () { - Debug.WriteLine ($"ConfigurationManager.ToJson()"); - return JsonSerializer.Serialize (Settings!, _serializerOptions); + Debug.WriteLine ("ConfigurationManager.ToJson()"); + return JsonSerializer.Serialize (Settings!, _serializerOptions); } internal static Stream ToStream () { - var json = JsonSerializer.Serialize (Settings!, _serializerOptions); + var json = JsonSerializer.Serialize (Settings!, _serializerOptions); // turn it into a stream var stream = new MemoryStream (); var writer = new StreamWriter (stream); @@ -207,16 +259,6 @@ internal static Stream ToStream () return stream; } - /// - /// Gets or sets whether the should throw an exception if it encounters - /// an error on deserialization. If (the default), the error is logged and printed to the - /// console when is called. - /// - [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] - public static bool? ThrowOnJsonErrors { get; set; } = false; - - internal static StringBuilder jsonErrors = new StringBuilder (); - internal static void AddJsonError (string error) { Debug.WriteLine ($"ConfigurationManager: {error}"); @@ -229,15 +271,12 @@ internal static void AddJsonError (string error) public static void PrintJsonErrors () { if (jsonErrors.Length > 0) { - Console.WriteLine ($"Terminal.Gui ConfigurationManager encountered the following errors while deserializing configuration files:"); + Console.WriteLine (@"Terminal.Gui ConfigurationManager encountered the following errors while deserializing configuration files:"); Console.WriteLine (jsonErrors.ToString ()); } } - private static void ClearJsonErrors () - { - jsonErrors.Clear (); - } + static void ClearJsonErrors () => jsonErrors.Clear (); /// /// Called when the configuration has been updated from a configuration file. Invokes the @@ -245,12 +284,12 @@ private static void ClearJsonErrors () /// public static void OnUpdated () { - Debug.WriteLine ($"ConfigurationManager.OnApplied()"); + Debug.WriteLine (@"ConfigurationManager.OnApplied()"); Updated?.Invoke (null, new ConfigurationManagerEventArgs ()); } /// - /// Event fired when the configuration has been updated from a configuration source. + /// Event fired when the configuration has been updated from a configuration source. /// application. /// public static event EventHandler? Updated; @@ -265,9 +304,9 @@ public static void OnUpdated () /// public static void Reset () { - Debug.WriteLine ($"ConfigurationManager.Reset()"); + Debug.WriteLine (@"ConfigurationManager.Reset()"); if (_allConfigProperties == null) { - ConfigurationManager.Initialize (); + Initialize (); } ClearJsonErrors (); @@ -291,15 +330,16 @@ public static void Reset () /// is set to . /// /// - /// - /// This method is only really useful when using ConfigurationManagerTests - /// to generate the JSON doc that is embedded into Terminal.Gui (during development). - /// - /// - /// WARNING: The Terminal.Gui.Resources.config.json resource has setting definitions (Themes) - /// that are NOT generated by this function. If you use this function to regenerate Terminal.Gui.Resources.config.json, - /// make sure you copy the Theme definitions from the existing Terminal.Gui.Resources.config.json file. - /// + /// + /// This method is only really useful when using ConfigurationManagerTests + /// to generate the JSON doc that is embedded into Terminal.Gui (during development). + /// + /// + /// WARNING: The Terminal.Gui.Resources.config.json resource has setting definitions (Themes) + /// that are NOT generated by this function. If you use this function to regenerate + /// Terminal.Gui.Resources.config.json, + /// make sure you copy the Theme definitions from the existing Terminal.Gui.Resources.config.json file. + /// /// internal static void GetHardCodedDefaults () { @@ -341,12 +381,12 @@ public static void Apply () } /// - /// Called when an updated configuration has been applied to the + /// Called when an updated configuration has been applied to the /// application. Fires the event. /// public static void OnApplied () { - Debug.WriteLine ($"ConfigurationManager.OnApplied()"); + Debug.WriteLine ("ConfigurationManager.OnApplied()"); Applied?.Invoke (null, new ConfigurationManagerEventArgs ()); // TODO: Refactor ConfigurationManager to not use an event handler for this. @@ -355,62 +395,26 @@ public static void OnApplied () } /// - /// Event fired when an updated configuration has been applied to the + /// Event fired when an updated configuration has been applied to the /// application. /// public static event EventHandler? Applied; /// - /// Name of the running application. By default this property is set to the application's assembly name. - /// - public static string AppName { get; set; } = Assembly.GetEntryAssembly ()?.FullName?.Split (',') [0]?.Trim ()!; - - /// - /// Describes the location of the configuration files. The constants can be - /// combined (bitwise) to specify multiple locations. - /// - [Flags] - public enum ConfigLocations { - /// - /// No configuration will be loaded. - /// - /// - /// Used for development and testing only. For Terminal,Gui to function properly, at least - /// should be set. - /// - None = 0, - - /// - /// Global configuration in Terminal.Gui.dll's resources (Terminal.Gui.Resources.config.json) -- Lowest Precidence. - /// - DefaultOnly, - - /// - /// This constant is a combination of all locations - /// - All = -1 - - } - - /// - /// Gets and sets the locations where will look for config files. - /// The value is . - /// - public static ConfigLocations Locations { get; set; } = ConfigLocations.All; - - /// - /// Loads all settings found in the various configuration storage locations to + /// Loads all settings found in the various configuration storage locations to /// the . Optionally, /// resets all settings attributed with to the defaults. /// /// /// Use to cause the loaded settings to be applied to the running application. /// - /// If the state of will - /// be reset to the defaults. + /// + /// If the state of will + /// be reset to the defaults. + /// public static void Load (bool reset = false) { - Debug.WriteLine ($"ConfigurationManager.Load()"); + Debug.WriteLine ("ConfigurationManager.Load()"); if (reset) { Reset (); @@ -446,16 +450,16 @@ public static string GetEmptyJson () { var emptyScope = new SettingsScope (); emptyScope.Clear (); - return JsonSerializer.Serialize (emptyScope, _serializerOptions); + return JsonSerializer.Serialize (emptyScope, _serializerOptions); } /// /// System.Text.Json does not support copying a deserialized object to an existing instance. - /// To work around this, we implement a 'deep, memberwise copy' method. + /// To work around this, we implement a 'deep, memberwise copy' method. /// /// /// TOOD: When System.Text.Json implements `PopulateObject` revisit - /// https://github.com/dotnet/corefx/issues/37627 + /// https://github.com/dotnet/corefx/issues/37627 /// /// /// @@ -488,12 +492,10 @@ public static string GetEmptyJson () // Dictionary if (source.GetType ().IsGenericType && source.GetType ().GetGenericTypeDefinition ().IsAssignableFrom (typeof (Dictionary<,>))) { foreach (var srcKey in ((IDictionary)source).Keys) { - if (srcKey is string) { - - } - if (((IDictionary)destination).Contains (srcKey)) + if (srcKey is string) { } + if (((IDictionary)destination).Contains (srcKey)) { ((IDictionary)destination) [srcKey] = DeepMemberwiseCopy (((IDictionary)source) [srcKey], ((IDictionary)destination) [srcKey]); - else { + } else { ((IDictionary)destination).Add (srcKey, ((IDictionary)source) [srcKey]); } } @@ -503,7 +505,7 @@ public static string GetEmptyJson () // ALl other object types var sourceProps = source?.GetType ().GetProperties ().Where (x => x.CanRead).ToList (); var destProps = destination?.GetType ().GetProperties ().Where (x => x.CanWrite).ToList ()!; - foreach (var (sourceProp, destProp) in + foreach ((var sourceProp, var destProp) in from sourceProp in sourceProps where destProps.Any (x => x.Name == sourceProp.Name) let destProp = destProps.First (x => x.Name == sourceProp.Name) @@ -513,65 +515,18 @@ where destProp.CanWrite var sourceVal = sourceProp.GetValue (source); var destVal = destProp.GetValue (destination); if (sourceVal != null) { - if (destVal != null) { - // Recurse - destProp.SetValue (destination, DeepMemberwiseCopy (sourceVal, destVal)); - } else { - destProp.SetValue (destination, sourceVal); + try { + if (destVal != null) { + // Recurse + destProp.SetValue (destination, DeepMemberwiseCopy (sourceVal, destVal)); + } else { + destProp.SetValue (destination, sourceVal); + } + } catch (ArgumentException e) { + throw new JsonException ($"Error Applying Configuration Change: {e.Message}", e); } } } return destination!; } - - //public class ConfiguraitonLocation - //{ - // public string Name { get; set; } = string.Empty; - - // public string? Path { get; set; } - - // public async Task UpdateAsync (Stream stream) - // { - // var scope = await JsonSerializer.DeserializeAsync (stream, serializerOptions); - // if (scope != null) { - // ConfigurationManager.Settings?.UpdateFrom (scope); - // return scope; - // } - // return new SettingsScope (); - // } - - //} - - //public class StreamConfiguration { - // private bool _reset; - - // public StreamConfiguration (bool reset) - // { - // _reset = reset; - // } - - // public StreamConfiguration UpdateAppResources () - // { - // if (Locations.HasFlag (ConfigLocations.AppResources)) LoadAppResources (); - // return this; - // } - - // public StreamConfiguration UpdateAppDirectory () - // { - // if (Locations.HasFlag (ConfigLocations.AppDirectory)) LoadAppDirectory (); - // return this; - // } - - // // Additional update methods for each location here - - // private void LoadAppResources () - // { - // // Load AppResources logic here - // } - - // private void LoadAppDirectory () - // { - // // Load AppDirectory logic here - // } - //} -} +} \ No newline at end of file diff --git a/Terminal.Gui/Drawing/Color.cs b/Terminal.Gui/Drawing/Color.cs index bcab0b27f0..65ded54fa2 100644 --- a/Terminal.Gui/Drawing/Color.cs +++ b/Terminal.Gui/Drawing/Color.cs @@ -15,82 +15,100 @@ namespace Terminal.Gui; /// foreground and background colors in Terminal.Gui apps. Used with . /// /// -/// -/// These colors match the 16 colors defined for ANSI escape sequences for 4-bit (16) colors. -/// -/// -/// For terminals that support 24-bit color (TrueColor), the RGB values for each of these colors can be configured using the -/// property. -/// +/// +/// These colors match the 16 colors defined for ANSI escape sequences for 4-bit (16) colors. +/// +/// +/// For terminals that support 24-bit color (TrueColor), the RGB values for each of these colors can be configured +/// using the +/// property. +/// /// public enum ColorName { /// /// The black color. ANSI escape sequence: \u001b[30m. /// Black, + /// /// The blue color. ANSI escape sequence: \u001b[34m. /// Blue, + /// /// The green color. ANSI escape sequence: \u001b[32m. /// Green, + /// /// The cyan color. ANSI escape sequence: \u001b[36m. /// Cyan, + /// /// The red color. ANSI escape sequence: \u001b[31m. /// Red, + /// /// The magenta color. ANSI escape sequence: \u001b[35m. /// Magenta, + /// /// The yellow color (also known as Brown). ANSI escape sequence: \u001b[33m. /// Yellow, + /// /// The gray color (also known as White). ANSI escape sequence: \u001b[37m. /// Gray, + /// /// The dark gray color (also known as Bright Black). ANSI escape sequence: \u001b[30;1m. /// DarkGray, + /// /// The bright blue color. ANSI escape sequence: \u001b[34;1m. /// BrightBlue, + /// /// The bright green color. ANSI escape sequence: \u001b[32;1m. /// BrightGreen, + /// /// The bright cyan color. ANSI escape sequence: \u001b[36;1m. /// BrightCyan, + /// /// The bright red color. ANSI escape sequence: \u001b[31;1m. /// BrightRed, + /// /// The bright magenta color. ANSI escape sequence: \u001b[35;1m. /// BrightMagenta, + /// /// The bright yellow color. ANSI escape sequence: \u001b[33;1m. /// BrightYellow, + /// /// The White color (also known as Bright White). ANSI escape sequence: \u001b[37;1m. /// White } + /// -/// The 16 foreground color codes used by ANSI Esc sequences for 256 color terminals. Add 10 to these values for background color. +/// The 16 foreground color codes used by ANSI Esc sequences for 256 color terminals. Add 10 to these values for background +/// color. /// public enum AnsiColorCode { /// @@ -102,69 +120,134 @@ public enum AnsiColorCode { /// The ANSI color code for Red. /// RED = 31, + /// /// The ANSI color code for Green. /// GREEN = 32, + /// /// The ANSI color code for Yellow. /// YELLOW = 33, + /// /// The ANSI color code for Blue. /// BLUE = 34, + /// /// The ANSI color code for Magenta. /// MAGENTA = 35, + /// /// The ANSI color code for Cyan. /// CYAN = 36, + /// /// The ANSI color code for White. /// WHITE = 37, + /// /// The ANSI color code for Bright Black. /// BRIGHT_BLACK = 90, + /// /// The ANSI color code for Bright Red. /// BRIGHT_RED = 91, + /// /// The ANSI color code for Bright Green. /// BRIGHT_GREEN = 92, + /// /// The ANSI color code for Bright Yellow. /// BRIGHT_YELLOW = 93, + /// /// The ANSI color code for Bright Blue. /// BRIGHT_BLUE = 94, + /// /// The ANSI color code for Bright Magenta. /// BRIGHT_MAGENTA = 95, + /// /// The ANSI color code for Bright Cyan. /// BRIGHT_CYAN = 96, + /// /// The ANSI color code for Bright White. /// BRIGHT_WHITE = 97 } + /// -/// Represents a 24-bit color. Provides automatic mapping between the legacy 4-bit (16 color) system and 24-bit colors (see ). -/// Used with . +/// Represents a 24-bit color. Provides automatic mapping between the legacy 4-bit (16 color) system and 24-bit colors (see +/// ). Used with . /// [JsonConverter (typeof (ColorJsonConverter))] -public class Color : IEquatable { +public readonly struct Color : IEquatable { + + // TODO: Make this map configurable via ConfigurationManager + // TODO: This does not need to be a Dictionary, but can be an 16 element array. + /// + /// Maps legacy 16-color values to the corresponding 24-bit RGB value. + /// + internal static ImmutableDictionary _colorToNameMap = new Dictionary { + // using "Windows 10 Console/PowerShell 6" here: https://i.stack.imgur.com/9UVnC.png + // See also: https://en.wikipedia.org/wiki/ANSI_escape_code + { new Color (12, 12, 12), ColorName.Black }, + { new Color (0, 55, 218), ColorName.Blue }, + { new Color (19, 161, 14), ColorName.Green }, + { new Color (58, 150, 221), ColorName.Cyan }, + { new Color (197, 15, 31), ColorName.Red }, + { new Color (136, 23, 152), ColorName.Magenta }, + { new Color (128, 64, 32), ColorName.Yellow }, + { new Color (204, 204, 204), ColorName.Gray }, + { new Color (118, 118, 118), ColorName.DarkGray }, + { new Color (59, 120, 255), ColorName.BrightBlue }, + { new Color (22, 198, 12), ColorName.BrightGreen }, + { new Color (97, 214, 214), ColorName.BrightCyan }, + { new Color (231, 72, 86), ColorName.BrightRed }, + { new Color (180, 0, 158), ColorName.BrightMagenta }, + { new Color (249, 241, 165), ColorName.BrightYellow }, + { new Color (242, 242, 242), ColorName.White } + }.ToImmutableDictionary (); + + + /// + /// Defines the 16 legacy color names and values that can be used to set the + /// + internal static ImmutableDictionary _colorNameToAnsiColorMap = new Dictionary { + { ColorName.Black, AnsiColorCode.BLACK }, + { ColorName.Blue, AnsiColorCode.BLUE }, + { ColorName.Green, AnsiColorCode.GREEN }, + { ColorName.Cyan, AnsiColorCode.CYAN }, + { ColorName.Red, AnsiColorCode.RED }, + { ColorName.Magenta, AnsiColorCode.MAGENTA }, + { ColorName.Yellow, AnsiColorCode.YELLOW }, + { ColorName.Gray, AnsiColorCode.WHITE }, + { ColorName.DarkGray, AnsiColorCode.BRIGHT_BLACK }, + { ColorName.BrightBlue, AnsiColorCode.BRIGHT_BLUE }, + { ColorName.BrightGreen, AnsiColorCode.BRIGHT_GREEN }, + { ColorName.BrightCyan, AnsiColorCode.BRIGHT_CYAN }, + { ColorName.BrightRed, AnsiColorCode.BRIGHT_RED }, + { ColorName.BrightMagenta, AnsiColorCode.BRIGHT_MAGENTA }, + { ColorName.BrightYellow, AnsiColorCode.BRIGHT_YELLOW }, + { ColorName.White, AnsiColorCode.BRIGHT_WHITE } + }.ToImmutableDictionary (); + /// /// Initializes a new instance of the class. /// @@ -184,7 +267,13 @@ public Color (int red, int green, int blue, int alpha = 0xFF) /// Initializes a new instance of the class with an encoded 24-bit color value. /// /// The encoded 24-bit color value (see ). - public Color (int rgba) => Rgba = rgba; + public Color (int rgba) + { + A = (byte)(rgba >> 24 & 0xFF); + R = (byte)(rgba >> 16 & 0xFF); + G = (byte)(rgba >> 8 & 0xFF); + B = (byte)(rgba & 0xFF); + } /// /// Initializes a new instance of the color from a legacy 16-color value. @@ -200,7 +289,8 @@ public Color (ColorName colorName) } /// - /// Initializes a new instance of the color from string. See for details. + /// Initializes a new instance of the color from string. See + /// for details. /// /// /// @@ -229,15 +319,17 @@ public Color () /// /// Red color component. /// - public int R { get; set; } + public int R { get; } + /// /// Green color component. /// - public int G { get; set; } + public int G { get; } + /// /// Blue color component. /// - public int B { get; set; } + public int B { get; } /// /// Alpha color component. @@ -245,7 +337,7 @@ public Color () /// /// The Alpha channel is not supported by Terminal.Gui. /// - public int A { get; set; } = 0xFF; // Not currently supported; here for completeness. + public int A { get; } // Not currently supported; here for completeness. /// /// Gets or sets the color value encoded as ARGB32. @@ -253,64 +345,8 @@ public Color () /// (<see cref="A"/> << 24) | (<see cref="R"/> << 16) | (<see cref="G"/> << 8) | <see cref="B"/> /// /// - public int Rgba { - get => A << 24 | R << 16 | G << 8 | B; - set { - A = (byte)(value >> 24 & 0xFF); - R = (byte)(value >> 16 & 0xFF); - G = (byte)(value >> 8 & 0xFF); - B = (byte)(value & 0xFF); - } - } - - // TODO: Make this map configurable via ConfigurationManager - // TODO: This does not need to be a Dictionary, but can be an 16 element array. - /// - /// Maps legacy 16-color values to the corresponding 24-bit RGB value. - /// - internal static ImmutableDictionary _colorToNameMap = new Dictionary () { - // using "Windows 10 Console/PowerShell 6" here: https://i.stack.imgur.com/9UVnC.png - // See also: https://en.wikipedia.org/wiki/ANSI_escape_code - { new Color (12, 12, 12), ColorName.Black }, - { new Color (0, 55, 218), ColorName.Blue }, - { new Color (19, 161, 14), ColorName.Green }, - { new Color (58, 150, 221), ColorName.Cyan }, - { new Color (197, 15, 31), ColorName.Red }, - { new Color (136, 23, 152), ColorName.Magenta }, - { new Color (128, 64, 32), ColorName.Yellow }, - { new Color (204, 204, 204), ColorName.Gray }, - { new Color (118, 118, 118), ColorName.DarkGray }, - { new Color (59, 120, 255), ColorName.BrightBlue }, - { new Color (22, 198, 12), ColorName.BrightGreen }, - { new Color (97, 214, 214), ColorName.BrightCyan }, - { new Color (231, 72, 86), ColorName.BrightRed }, - { new Color (180, 0, 158), ColorName.BrightMagenta }, - { new Color (249, 241, 165), ColorName.BrightYellow }, - { new Color (242, 242, 242), ColorName.White } - }.ToImmutableDictionary (); - - - /// - /// Defines the 16 legacy color names and values that can be used to set the - /// - internal static ImmutableDictionary _colorNameToAnsiColorMap = new Dictionary { - { ColorName.Black, AnsiColorCode.BLACK }, - { ColorName.Blue, AnsiColorCode.BLUE }, - { ColorName.Green, AnsiColorCode.GREEN }, - { ColorName.Cyan, AnsiColorCode.CYAN }, - { ColorName.Red, AnsiColorCode.RED }, - { ColorName.Magenta, AnsiColorCode.MAGENTA }, - { ColorName.Yellow, AnsiColorCode.YELLOW }, - { ColorName.Gray, AnsiColorCode.WHITE }, - { ColorName.DarkGray, AnsiColorCode.BRIGHT_BLACK }, - { ColorName.BrightBlue, AnsiColorCode.BRIGHT_BLUE }, - { ColorName.BrightGreen, AnsiColorCode.BRIGHT_GREEN }, - { ColorName.BrightCyan, AnsiColorCode.BRIGHT_CYAN }, - { ColorName.BrightRed, AnsiColorCode.BRIGHT_RED }, - { ColorName.BrightMagenta, AnsiColorCode.BRIGHT_MAGENTA }, - { ColorName.BrightYellow, AnsiColorCode.BRIGHT_YELLOW }, - { ColorName.White, AnsiColorCode.BRIGHT_WHITE } - }.ToImmutableDictionary (); + [JsonIgnore] + public int Rgba => A << 24 | R << 16 | G << 8 | B; /// /// Gets or sets the 24-bit color value for each of the legacy 16-color values. @@ -332,6 +368,28 @@ public static Dictionary Colors { } } + /// + /// Gets the using a legacy 16-color value. + /// will return the closest 16 color match to the true color when no exact value is found. + /// + /// + /// Get returns the of the closest 24-bit color value. Set sets the RGB value using a hard-coded + /// map. + /// + [JsonIgnore] + public ColorName ColorName => FindClosestColor (this); + + /// + /// Gets the using a legacy 16-color value. + /// will return the closest 16 color match to the true color when no exact value is found. + /// + /// + /// Get returns the of the closest 24-bit color value. Set sets the RGB value using a hard-coded + /// map. + /// + [JsonIgnore] + public AnsiColorCode AnsiColorCode => _colorNameToAnsiColorMap [ColorName]; + /// /// Converts a legacy to a 24-bit . /// @@ -346,10 +404,10 @@ public static Dictionary Colors { internal static ColorName FindClosestColor (Color inputColor) { var closestColor = ColorName.Black; // Default to Black - double closestDistance = double.MaxValue; + var closestDistance = double.MaxValue; foreach (var colorEntry in _colorToNameMap) { - double distance = CalculateColorDistance (inputColor, colorEntry.Key); + var distance = CalculateColorDistance (inputColor, colorEntry.Key); if (distance < closestDistance) { closestDistance = distance; closestColor = colorEntry.Value; @@ -362,41 +420,128 @@ internal static ColorName FindClosestColor (Color inputColor) static double CalculateColorDistance (Color color1, Color color2) { // Calculate the Euclidean distance between two colors - double deltaR = (double)color1.R - (double)color2.R; - double deltaG = (double)color1.G - (double)color2.G; - double deltaB = (double)color1.B - (double)color2.B; + var deltaR = color1.R - (double)color2.R; + var deltaG = color1.G - (double)color2.G; + var deltaB = color1.B - (double)color2.B; return Math.Sqrt (deltaR * deltaR + deltaG * deltaG + deltaB * deltaB); } /// - /// Gets or sets the using a legacy 16-color value. - /// will return the closest 16 color match to the true color when no exact value is found. + /// Converts the provided string to a new instance. /// + /// + /// The text to analyze. Formats supported are + /// "#RGB", "#RRGGBB", "#RGBA", "#RRGGBBAA", "rgb(r,g,b)", "rgb(r,g,b,a)", and any of the + /// . + /// + /// The parsed value. + /// A boolean value indicating whether parsing was successful. /// - /// Get returns the of the closest 24-bit color value. Set sets the RGB value using a hard-coded map. + /// While supports the alpha channel , Terminal.Gui does not. /// - public ColorName ColorName { - get => FindClosestColor (this); - set { + public static bool TryParse (string text, [NotNullWhen (true)] out Color color) + { + // empty color + if (string.IsNullOrEmpty (text)) { + color = new Color (); + return false; + } - var c = FromColorName (value); - R = c.R; - G = c.G; - B = c.B; - A = c.A; + // #RRGGBB, #RGB + if (text [0] == '#' && text.Length is 7 or 4) { + if (text.Length == 7) { + var r = Convert.ToInt32 (text.Substring (1, 2), 16); + var g = Convert.ToInt32 (text.Substring (3, 2), 16); + var b = Convert.ToInt32 (text.Substring (5, 2), 16); + color = new Color (r, g, b); + } else { + var rText = char.ToString (text [1]); + var gText = char.ToString (text [2]); + var bText = char.ToString (text [3]); + + var r = Convert.ToInt32 (rText + rText, 16); + var g = Convert.ToInt32 (gText + gText, 16); + var b = Convert.ToInt32 (bText + bText, 16); + color = new Color (r, g, b); + } + return true; + } + + // #RRGGBB, #RGBA + if (text [0] == '#' && text.Length is 8 or 5) { + if (text.Length == 7) { + var r = Convert.ToInt32 (text.Substring (1, 2), 16); + var g = Convert.ToInt32 (text.Substring (3, 2), 16); + var b = Convert.ToInt32 (text.Substring (5, 2), 16); + var a = Convert.ToInt32 (text.Substring (7, 2), 16); + color = new Color (a, r, g, b); + } else { + var rText = char.ToString (text [1]); + var gText = char.ToString (text [2]); + var bText = char.ToString (text [3]); + var aText = char.ToString (text [4]); + + var r = Convert.ToInt32 (aText + aText, 16); + var g = Convert.ToInt32 (rText + rText, 16); + var b = Convert.ToInt32 (gText + gText, 16); + var a = Convert.ToInt32 (bText + bText, 16); + color = new Color (r, g, b, a); + } + return true; + } + + // rgb(r,g,b) + var match = Regex.Match (text, @"rgb\((\d+),(\d+),(\d+)\)"); + if (match.Success) { + var r = int.Parse (match.Groups [1].Value); + var g = int.Parse (match.Groups [2].Value); + var b = int.Parse (match.Groups [3].Value); + color = new Color (r, g, b); + return true; + } + + // rgb(r,g,b,a) + match = Regex.Match (text, @"rgb\((\d+),(\d+),(\d+),(\d+)\)"); + if (match.Success) { + var r = int.Parse (match.Groups [1].Value); + var g = int.Parse (match.Groups [2].Value); + var b = int.Parse (match.Groups [3].Value); + var a = int.Parse (match.Groups [4].Value); + color = new Color (r, g, b, a); + return true; + } + + if (Enum.TryParse (text, true, out var colorName)) { + color = new Color (colorName); + return true; } + + color = new Color (); + return false; } /// - /// Gets or sets the using a legacy 16-color value. - /// will return the closest 16 color match to the true color when no exact value is found. + /// Converts the color to a string representation. /// /// - /// Get returns the of the closest 24-bit color value. Set sets the RGB value using a hard-coded map. + /// + /// If the color is a named color, the name is returned. Otherwise, the color is returned as a hex string. + /// + /// + /// (Alpha channel) is ignored and the returned string will not include it. + /// /// - [JsonIgnore] - public AnsiColorCode AnsiColorCode => _colorNameToAnsiColorMap [ColorName]; + /// + public override string ToString () + { + // If Values has an exact match with a named color (in _colorNames), use that. + if (_colorToNameMap.TryGetValue (this, out var colorName)) { + return Enum.GetName (typeof (ColorName), colorName); + } + // Otherwise return as an RGB hex value. + return $"#{R:X2}{G:X2}{B:X2}"; + } #region Legacy Color Names /// @@ -408,162 +553,85 @@ public ColorName ColorName { /// The blue color. /// public const ColorName Blue = ColorName.Blue; + /// /// The green color. /// public const ColorName Green = ColorName.Green; + /// /// The cyan color. /// public const ColorName Cyan = ColorName.Cyan; + /// /// The red color. /// public const ColorName Red = ColorName.Red; + /// /// The magenta color. /// public const ColorName Magenta = ColorName.Magenta; + /// /// The yellow color. /// public const ColorName Yellow = ColorName.Yellow; + /// /// The gray color. /// public const ColorName Gray = ColorName.Gray; + /// /// The dark gray color. /// public const ColorName DarkGray = ColorName.DarkGray; + /// /// The bright bBlue color. /// public const ColorName BrightBlue = ColorName.BrightBlue; + /// /// The bright green color. /// public const ColorName BrightGreen = ColorName.BrightGreen; + /// /// The bright cyan color. /// public const ColorName BrightCyan = ColorName.BrightCyan; + /// /// The bright red color. /// public const ColorName BrightRed = ColorName.BrightRed; + /// /// The bright magenta color. /// public const ColorName BrightMagenta = ColorName.BrightMagenta; + /// /// The bright yellow color. /// public const ColorName BrightYellow = ColorName.BrightYellow; + /// /// The White color. /// public const ColorName White = ColorName.White; #endregion - /// - /// Converts the provided string to a new instance. - /// - /// The text to analyze. Formats supported are - /// "#RGB", "#RRGGBB", "#RGBA", "#RRGGBBAA", "rgb(r,g,b)", "rgb(r,g,b,a)", and any of the - /// . - /// The parsed value. - /// A boolean value indicating whether parsing was successful. - /// - /// While supports the alpha channel , Terminal.Gui does not. - /// - public static bool TryParse (string text, [NotNullWhen (true)] out Color color) - { - // empty color - if (text == null || text.Length == 0) { - color = null; - return false; - } - - // #RRGGBB, #RGB - if (text [0] == '#' && text.Length is 7 or 4) { - if (text.Length == 7) { - int r = Convert.ToInt32 (text.Substring (1, 2), 16); - int g = Convert.ToInt32 (text.Substring (3, 2), 16); - int b = Convert.ToInt32 (text.Substring (5, 2), 16); - color = new Color (r, g, b); - } else { - string rText = char.ToString (text [1]); - string gText = char.ToString (text [2]); - string bText = char.ToString (text [3]); - - int r = Convert.ToInt32 (rText + rText, 16); - int g = Convert.ToInt32 (gText + gText, 16); - int b = Convert.ToInt32 (bText + bText, 16); - color = new Color (r, g, b); - } - return true; - } - - // #RRGGBB, #RGBA - if (text [0] == '#' && text.Length is 8 or 5) { - if (text.Length == 7) { - int r = Convert.ToInt32 (text.Substring (1, 2), 16); - int g = Convert.ToInt32 (text.Substring (3, 2), 16); - int b = Convert.ToInt32 (text.Substring (5, 2), 16); - int a = Convert.ToInt32 (text.Substring (7, 2), 16); - color = new Color (a, r, g, b); - } else { - string rText = char.ToString (text [1]); - string gText = char.ToString (text [2]); - string bText = char.ToString (text [3]); - string aText = char.ToString (text [4]); - - int r = Convert.ToInt32 (aText + aText, 16); - int g = Convert.ToInt32 (rText + rText, 16); - int b = Convert.ToInt32 (gText + gText, 16); - int a = Convert.ToInt32 (bText + bText, 16); - color = new Color (r, g, b, a); - } - return true; - } - - // rgb(r,g,b) - var match = Regex.Match (text, @"rgb\((\d+),(\d+),(\d+)\)"); - if (match.Success) { - int r = int.Parse (match.Groups [1].Value); - int g = int.Parse (match.Groups [2].Value); - int b = int.Parse (match.Groups [3].Value); - color = new Color (r, g, b); - return true; - } - - // rgb(r,g,b,a) - match = Regex.Match (text, @"rgb\((\d+),(\d+),(\d+),(\d+)\)"); - if (match.Success) { - int r = int.Parse (match.Groups [1].Value); - int g = int.Parse (match.Groups [2].Value); - int b = int.Parse (match.Groups [3].Value); - int a = int.Parse (match.Groups [4].Value); - color = new Color (r, g, b, a); - return true; - } - - if (Enum.TryParse (text, true, out var colorName)) { - color = new Color (colorName); - return true; - } - - color = null; - return false; - } - + // TODO: Verify implict/explicit are correct for below #region Operators /// /// Cast from int. /// /// - public static implicit operator Color (int rgba) => new Color (rgba); + public static implicit operator Color (int rgba) => new (rgba); /// /// Cast to int. @@ -575,7 +643,7 @@ public static bool TryParse (string text, [NotNullWhen (true)] out Color color) /// Cast from . /// /// - public static explicit operator Color (ColorName colorName) => new Color (colorName); + public static explicit operator Color (ColorName colorName) => new (colorName); /// /// Cast to . @@ -590,18 +658,7 @@ public static bool TryParse (string text, [NotNullWhen (true)] out Color color) /// /// /// - public static bool operator == (Color left, Color right) - { - if (left is null && right is null) { - return true; - } - - if (left is null || right is null) { - return false; - } - - return left.Equals (right); - } + public static bool operator == (Color left, Color right) => left.Equals (right); /// @@ -610,18 +667,7 @@ public static bool TryParse (string text, [NotNullWhen (true)] out Color color) /// /// /// - public static bool operator != (Color left, Color right) - { - if (left is null && right is null) { - return false; - } - - if (left is null || right is null) { - return true; - } - - return !left.Equals (right); - } + public static bool operator != (Color left, Color right) => !left.Equals (right); /// /// Equality operator for and objects. @@ -661,50 +707,31 @@ public static bool TryParse (string text, [NotNullWhen (true)] out Color color) /// public bool Equals (Color other) => R == other.R && - G == other.G && - B == other.B && - A == other.A; + G == other.G && + B == other.B && + A == other.A; /// public override int GetHashCode () => HashCode.Combine (R, G, B, A); #endregion - - /// - /// Converts the color to a string representation. - /// - /// - /// - /// If the color is a named color, the name is returned. Otherwise, the color is returned as a hex string. - /// - /// - /// (Alpha channel) is ignored and the returned string will not include it. - /// - /// - /// - public override string ToString () - { - // If Values has an exact match with a named color (in _colorNames), use that. - if (_colorToNameMap.TryGetValue (this, out var colorName)) { - return Enum.GetName (typeof (ColorName), colorName); - } - // Otherwise return as an RGB hex value. - return $"#{R:X2}{G:X2}{B:X2}"; - } } + /// -/// Attributes represent how text is styled when displayed in the terminal. +/// Attributes represent how text is styled when displayed in the terminal. /// /// -/// provides a platform independent representation of colors (and someday other forms of text styling). -/// They encode both the foreground and the background color and are used in the -/// class to define color schemes that can be used in an application. +/// provides a platform independent representation of colors (and someday other forms of text +/// styling). +/// They encode both the foreground and the background color and are used in the +/// class to define color schemes that can be used in an application. /// [JsonConverter (typeof (AttributeJsonConverter))] public readonly struct Attribute : IEquatable { /// /// Default empty attribute. /// - public static readonly Attribute Default = new Attribute (Color.White, Color.Black); + /// + public static readonly Attribute Default = new (Color.White, Color.Black); /// /// The -specific color value. @@ -716,23 +743,32 @@ public override string ToString () /// The foreground color. /// [JsonConverter (typeof (ColorJsonConverter))] - public Color Foreground { get; private init; } + public Color Foreground { get; } /// /// The background color. /// [JsonConverter (typeof (ColorJsonConverter))] - public Color Background { get; private init; } + public Color Background { get; } /// - /// Initializes a new instance with default values. + /// Initializes a new instance with default values. /// public Attribute () { PlatformColor = -1; - var d = Default; - Foreground = new Color (d.Foreground.ColorName); - Background = new Color (d.Background.ColorName); + Foreground = new Color (Default.Foreground.ColorName); + Background = new Color (Default.Background.ColorName); + } + + /// + /// Initializes a new instance from an existing instance. + /// + public Attribute (Attribute attr) + { + PlatformColor = -1; + Foreground = new Color (attr.Foreground.ColorName); + Background = new Color (attr.Background.ColorName); } /// @@ -742,9 +778,8 @@ public Attribute () internal Attribute (int platformColor) { PlatformColor = platformColor; - var d = Default; - Foreground = new Color (d.Foreground.ColorName); - Background = new Color (d.Background.ColorName); + Foreground = new Color (Default.Foreground.ColorName); + Background = new Color (Default.Background.ColorName); } /// @@ -819,7 +854,7 @@ public Attribute (ColorName foregroundName, Color background) : this (new Color /// /// Initializes a new instance of the struct - /// with the same colors for the foreground and background. + /// with the same colors for the foreground and background. /// /// The color. public Attribute (Color color) : this (color, color) { } @@ -841,24 +876,25 @@ public Attribute (Color color) : this (color, color) { } /// public static bool operator != (Attribute left, Attribute right) => !(left == right); - /// + /// public override bool Equals (object obj) => obj is Attribute other && Equals (other); - /// + /// public bool Equals (Attribute other) => PlatformColor == other.PlatformColor && - Foreground == other.Foreground && - Background == other.Background; + Foreground == other.Foreground && + Background == other.Background; - /// + /// public override int GetHashCode () => HashCode.Combine (PlatformColor, Foreground, Background); - /// + /// public override string ToString () => // Note, Unit tests are dependent on this format $"{Foreground},{Background}"; } + /// -/// Defines the s for common visible elements in a . +/// Defines the s for common visible elements in a . /// Containers such as and use to determine /// the colors used by sub-views. /// @@ -867,14 +903,15 @@ public override string ToString () => /// [JsonConverter (typeof (ColorSchemeJsonConverter))] public class ColorScheme : IEquatable { - Attribute _normal = Attribute.Default; + Attribute _disabled = Attribute.Default; Attribute _focus = Attribute.Default; - Attribute _hotNormal = Attribute.Default; Attribute _hotFocus = Attribute.Default; - Attribute _disabled = Attribute.Default; + Attribute _hotNormal = Attribute.Default; + Attribute _normal = Attribute.Default; /// - /// Used by and to track which ColorScheme + /// Used by and to + /// track which ColorScheme /// is being accessed. /// internal string _schemeBeingSet = ""; @@ -888,7 +925,7 @@ public ColorScheme () : this (Attribute.Default) { } /// Creates a new instance, initialized with the values from . /// /// The scheme to initialize the new instance with. - public ColorScheme (ColorScheme scheme) : base () + public ColorScheme (ColorScheme scheme) { if (scheme != null) { _normal = scheme.Normal; @@ -955,21 +992,21 @@ public Attribute Disabled { /// /// Compares two objects for equality. /// - /// + /// /// true if the two objects are equal - public override bool Equals (object obj) => Equals (obj as ColorScheme); + public bool Equals (ColorScheme other) => other != null && + EqualityComparer.Default.Equals (_normal, other._normal) && + EqualityComparer.Default.Equals (_focus, other._focus) && + EqualityComparer.Default.Equals (_hotNormal, other._hotNormal) && + EqualityComparer.Default.Equals (_hotFocus, other._hotFocus) && + EqualityComparer.Default.Equals (_disabled, other._disabled); /// /// Compares two objects for equality. /// - /// + /// /// true if the two objects are equal - public bool Equals (ColorScheme other) => other != null && - EqualityComparer.Default.Equals (_normal, other._normal) && - EqualityComparer.Default.Equals (_focus, other._focus) && - EqualityComparer.Default.Equals (_hotNormal, other._hotNormal) && - EqualityComparer.Default.Equals (_hotFocus, other._hotFocus) && - EqualityComparer.Default.Equals (_disabled, other._disabled); + public override bool Equals (object obj) => Equals (obj as ColorScheme); /// /// Returns a hashcode for this instance. @@ -977,7 +1014,7 @@ public bool Equals (ColorScheme other) => other != null && /// hashcode for this instance public override int GetHashCode () { - int hashCode = -1242460230; + var hashCode = -1242460230; hashCode = hashCode * -1521134295 + _normal.GetHashCode (); hashCode = hashCode * -1521134295 + _focus.GetHashCode (); hashCode = hashCode * -1521134295 + _hotNormal.GetHashCode (); @@ -1002,6 +1039,7 @@ public override int GetHashCode () /// true if the two objects are not equivalent public static bool operator != (ColorScheme left, ColorScheme right) => !(left == right); } + /// /// The default s for the application. /// @@ -1009,38 +1047,17 @@ public override int GetHashCode () /// This property can be set in a Theme to change the default for the application. /// public static class Colors { - class SchemeNameComparerIgnoreCase : IEqualityComparer { - public bool Equals (string x, string y) - { - if (x != null && y != null) { - return string.Equals (x, y, StringComparison.InvariantCultureIgnoreCase); - } - return false; - } - - public int GetHashCode (string obj) => obj.ToLowerInvariant ().GetHashCode (); - } static Colors () => ColorSchemes = Create (); - /// - /// Creates a new dictionary of new objects. - /// - public static Dictionary Create () => - // Use reflection to dynamically create the default set of ColorSchemes from the list defined - // by the class. - typeof (Colors).GetProperties () - .Where (p => p.PropertyType == typeof (ColorScheme)) - .Select (p => new KeyValuePair (p.Name, new ColorScheme ())) - .ToDictionary (t => t.Key, t => t.Value, new SchemeNameComparerIgnoreCase ()); - /// /// The application Toplevel color scheme, for the default Toplevel views. /// /// - /// - /// This API will be deprecated in the future. Use instead (e.g. edit.ColorScheme = Colors.ColorSchemes["TopLevel"]; - /// + /// + /// This API will be deprecated in the future. Use instead (e.g. + /// edit.ColorScheme = Colors.ColorSchemes["TopLevel"]; + /// /// public static ColorScheme TopLevel { get => GetColorScheme (); set => SetColorScheme (value); } @@ -1048,9 +1065,10 @@ public static Dictionary Create () => /// The base color scheme, for the default Toplevel views. /// /// - /// - /// This API will be deprecated in the future. Use instead (e.g. edit.ColorScheme = Colors.ColorSchemes["Base"]; - /// + /// + /// This API will be deprecated in the future. Use instead (e.g. + /// edit.ColorScheme = Colors.ColorSchemes["Base"]; + /// /// public static ColorScheme Base { get => GetColorScheme (); set => SetColorScheme (value); } @@ -1058,9 +1076,10 @@ public static Dictionary Create () => /// The dialog color scheme, for standard popup dialog boxes /// /// - /// - /// This API will be deprecated in the future. Use instead (e.g. edit.ColorScheme = Colors.ColorSchemes["Dialog"]; - /// + /// + /// This API will be deprecated in the future. Use instead (e.g. + /// edit.ColorScheme = Colors.ColorSchemes["Dialog"]; + /// /// public static ColorScheme Dialog { get => GetColorScheme (); set => SetColorScheme (value); } @@ -1068,9 +1087,10 @@ public static Dictionary Create () => /// The menu bar color /// /// - /// - /// This API will be deprecated in the future. Use instead (e.g. edit.ColorScheme = Colors.ColorSchemes["Menu"]; - /// + /// + /// This API will be deprecated in the future. Use instead (e.g. + /// edit.ColorScheme = Colors.ColorSchemes["Menu"]; + /// /// public static ColorScheme Menu { get => GetColorScheme (); set => SetColorScheme (value); } @@ -1078,12 +1098,31 @@ public static Dictionary Create () => /// The color scheme for showing errors. /// /// - /// - /// This API will be deprecated in the future. Use instead (e.g. edit.ColorScheme = Colors.ColorSchemes["Error"]; - /// + /// + /// This API will be deprecated in the future. Use instead (e.g. + /// edit.ColorScheme = Colors.ColorSchemes["Error"]; + /// /// public static ColorScheme Error { get => GetColorScheme (); set => SetColorScheme (value); } + /// + /// Provides the defined s. + /// + [SerializableConfigurationProperty (Scope = typeof (ThemeScope), OmitClassName = true)] + [JsonConverter (typeof (DictionaryJsonConverter))] + public static Dictionary ColorSchemes { get; private set; } // Serialization requires this to have a setter (private set;) + + /// + /// Creates a new dictionary of new objects. + /// + public static Dictionary Create () => + // Use reflection to dynamically create the default set of ColorSchemes from the list defined + // by the class. + typeof (Colors).GetProperties () + .Where (p => p.PropertyType == typeof (ColorScheme)) + .Select (p => new KeyValuePair (p.Name, new ColorScheme ())) + .ToDictionary (t => t.Key, t => t.Value, new SchemeNameComparerIgnoreCase ()); + static ColorScheme GetColorScheme ([CallerMemberName] string schemeBeingSet = null) => ColorSchemes [schemeBeingSet]; static void SetColorScheme (ColorScheme colorScheme, [CallerMemberName] string schemeBeingSet = null) @@ -1092,10 +1131,15 @@ static void SetColorScheme (ColorScheme colorScheme, [CallerMemberName] string s colorScheme._schemeBeingSet = schemeBeingSet; } - /// - /// Provides the defined s. - /// - [SerializableConfigurationProperty (Scope = typeof (ThemeScope), OmitClassName = true)] - [JsonConverter (typeof (DictionaryJsonConverter))] - public static Dictionary ColorSchemes { get; private set; } + class SchemeNameComparerIgnoreCase : IEqualityComparer { + public bool Equals (string x, string y) + { + if (x != null && y != null) { + return string.Equals (x, y, StringComparison.InvariantCultureIgnoreCase); + } + return false; + } + + public int GetHashCode (string obj) => obj.ToLowerInvariant ().GetHashCode (); + } } \ No newline at end of file diff --git a/Terminal.Gui/Views/FileSystemColorProvider.cs b/Terminal.Gui/Views/FileSystemColorProvider.cs index 6c78f23012..1196cf315e 100644 --- a/Terminal.Gui/Views/FileSystemColorProvider.cs +++ b/Terminal.Gui/Views/FileSystemColorProvider.cs @@ -14,7 +14,7 @@ public class FileSystemColorProvider { /// /// /// - public Color GetColor (IFileSystemInfo file) + public Color? GetColor (IFileSystemInfo file) { if (FilenameToColor.ContainsKey (file.Name)) { return FilenameToColor [file.Name]; @@ -443,8 +443,10 @@ public Color GetColor (IFileSystemInfo file) private static Color StringToColor (string str) { - Color.TryParse (str, out var c); - return c ?? throw new System.Exception ("Failed to parse Color from " + str); + if (!Color.TryParse (str, out var c)) { + throw new System.Exception ("Failed to parse Color from " + str); + } + return c; } } } \ No newline at end of file diff --git a/Terminal.Gui/Views/TreeView/TreeNode.cs b/Terminal.Gui/Views/TreeView/TreeNode.cs index 5ae07ae19c..a8769b6e94 100644 --- a/Terminal.Gui/Views/TreeView/TreeNode.cs +++ b/Terminal.Gui/Views/TreeView/TreeNode.cs @@ -1,73 +1,65 @@ using System.Collections.Generic; -namespace Terminal.Gui { - +namespace Terminal.Gui; + +/// +/// Interface to implement when you want the regular (non generic) +/// to automatically determine children for your class (without having to specify +/// an ) +/// +public interface ITreeNode { + /// + /// Text to display when rendering the node + /// + string Text { get; set; } + /// - /// Interface to implement when you want the regular (non generic) - /// to automatically determine children for your class (without having to specify - /// an ) + /// The children of your class which should be rendered underneath it when expanded /// - public interface ITreeNode { - /// - /// Text to display when rendering the node - /// - string Text { get; set; } + /// + IList Children { get; } - /// - /// The children of your class which should be rendered underneath it when expanded - /// - /// - IList Children { get; } + /// + /// Optionally allows you to store some custom data/class here. + /// + object Tag { get; set; } +} - /// - /// Optionally allows you to store some custom data/class here. - /// - object Tag { get; set; } - } +/// +/// Simple class for representing nodes, use with regular (non generic) . +/// +public class TreeNode : ITreeNode { /// - /// Simple class for representing nodes, use with regular (non generic) . + /// Initialises a new instance with no /// - public class TreeNode : ITreeNode { - /// - /// Children of the current node - /// - /// - public virtual IList Children { get; set; } = new List (); + public TreeNode () { } - /// - /// Text to display in tree node for current entry - /// - /// - public virtual string Text { get; set; } + /// + /// Initialises a new instance and sets starting + /// + public TreeNode (string text) => Text = text; - /// - /// Optionally allows you to store some custom data/class here. - /// - public object Tag { get; set; } + /// + /// Children of the current node + /// + /// + public virtual IList Children { get; set; } = new List (); - /// - /// returns - /// - /// - public override string ToString () - { - return Text ?? "Unamed Node"; - } + /// + /// Text to display in tree node for current entry + /// + /// + public virtual string Text { get; set; } - /// - /// Initialises a new instance with no - /// - public TreeNode () - { + /// + /// Optionally allows you to store some custom data/class here. + /// + public object Tag { get; set; } - } - /// - /// Initialises a new instance and sets starting - /// - public TreeNode (string text) - { - Text = text; - } - } + /// + /// returns + /// + /// + public override string ToString () => Text ?? "Unamed Node"; } \ No newline at end of file diff --git a/Terminal.Gui/Views/TreeView/TreeView.cs b/Terminal.Gui/Views/TreeView/TreeView.cs index b5ca8f4829..9632c81b86 100644 --- a/Terminal.Gui/Views/TreeView/TreeView.cs +++ b/Terminal.Gui/Views/TreeView/TreeView.cs @@ -2,1474 +2,1495 @@ // by phillip.piper@gmail.com). Phillip has explicitly granted permission for his design // and code to be used in this library under the MIT license. -using System.Text; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using Terminal.Gui; -namespace Terminal.Gui { +namespace Terminal.Gui; +/// +/// Interface for all non generic members of . +/// See TreeView Deep Dive for more information. +/// +public interface ITreeView { /// - /// Interface for all non generic members of . - /// - /// See TreeView Deep Dive for more information. - /// - public interface ITreeView { - /// - /// Contains options for changing how the tree is rendered. - /// - TreeStyle Style { get; set; } - - /// - /// Removes all objects from the tree and clears selection. - /// - void ClearObjects (); - - /// - /// Sets a flag indicating this view needs to be redisplayed because its state has changed. - /// - void SetNeedsDisplay (); + /// Contains options for changing how the tree is rendered. + /// + TreeStyle Style { get; set; } + + /// + /// Removes all objects from the tree and clears selection. + /// + void ClearObjects (); + + /// + /// Sets a flag indicating this view needs to be redisplayed because its state has changed. + /// + void SetNeedsDisplay (); +} + +/// +/// Convenience implementation of generic for any tree were all nodes +/// implement . +/// See TreeView Deep Dive for more information. +/// +public class TreeView : TreeView { + + /// + /// Creates a new instance of the tree control with absolute positioning and initialises + /// with default based builder. + /// + public TreeView () + { + TreeBuilder = new TreeNodeBuilder (); + AspectGetter = o => o == null ? "Null" : o.Text ?? o?.ToString () ?? "Unamed Node"; } +} + +/// +/// Hierarchical tree view with expandable branches. Branch objects are dynamically determined +/// when expanded using a user defined . +/// See TreeView Deep Dive for more information. +/// +public class TreeView : View, ITreeView where T : class { /// - /// Convenience implementation of generic for any tree were all nodes - /// implement . - /// - /// See TreeView Deep Dive for more information. + /// Error message to display when the control is not properly initialized at draw time + /// (nodes added but no tree builder set). /// - public class TreeView : TreeView { + public static string NoBuilderError = "ERROR: TreeBuilder Not Set"; - /// - /// Creates a new instance of the tree control with absolute positioning and initialises - /// with default based builder. - /// - public TreeView () - { - TreeBuilder = new TreeNodeBuilder (); - AspectGetter = o => o == null ? "Null" : (o.Text ?? o?.ToString () ?? "Unamed Node"); - } + /// + /// Cached result of + /// + IReadOnlyCollection> cachedLineMap; + + CursorVisibility desiredCursorVisibility = CursorVisibility.Invisible; + + /// + /// Interface for filtering which lines of the tree are displayed + /// e.g. to provide text searching. Defaults to + /// (no filtering). + /// + public ITreeViewFilter Filter = null; + + /// + /// Secondary selected regions of tree when is true. + /// + readonly Stack> multiSelectedRegions = new (); + + KeyCode objectActivationKey = KeyCode.Enter; + int scrollOffsetHorizontal; + int scrollOffsetVertical; + + /// + /// private variable for + /// + T selectedObject; + + /// + /// Creates a new tree view with absolute positioning. + /// Use to set set root objects for the tree. + /// Children will not be rendered until you set . + /// + public TreeView () + { + CanFocus = true; + + // Things this view knows how to do + AddCommand (Command.PageUp, () => { + MovePageUp (); + return true; + }); + AddCommand (Command.PageDown, () => { + MovePageDown (); + return true; + }); + AddCommand (Command.PageUpExtend, () => { + MovePageUp (true); + return true; + }); + AddCommand (Command.PageDownExtend, () => { + MovePageDown (true); + return true; + }); + AddCommand (Command.Expand, () => { + Expand (); + return true; + }); + AddCommand (Command.ExpandAll, () => { + ExpandAll (SelectedObject); + return true; + }); + AddCommand (Command.Collapse, () => { + CursorLeft (false); + return true; + }); + AddCommand (Command.CollapseAll, () => { + CursorLeft (true); + return true; + }); + AddCommand (Command.LineUp, () => { + AdjustSelection (-1); + return true; + }); + AddCommand (Command.LineUpExtend, () => { + AdjustSelection (-1, true); + return true; + }); + AddCommand (Command.LineUpToFirstBranch, () => { + AdjustSelectionToBranchStart (); + return true; + }); + + AddCommand (Command.LineDown, () => { + AdjustSelection (1); + return true; + }); + AddCommand (Command.LineDownExtend, () => { + AdjustSelection (1, true); + return true; + }); + AddCommand (Command.LineDownToLastBranch, () => { + AdjustSelectionToBranchEnd (); + return true; + }); + + AddCommand (Command.TopHome, () => { + GoToFirst (); + return true; + }); + AddCommand (Command.BottomEnd, () => { + GoToEnd (); + return true; + }); + AddCommand (Command.SelectAll, () => { + SelectAll (); + return true; + }); + + AddCommand (Command.ScrollUp, () => { + ScrollUp (); + return true; + }); + AddCommand (Command.ScrollDown, () => { + ScrollDown (); + return true; + }); + AddCommand (Command.Accept, () => { + ActivateSelectedObjectIfAny (); + return true; + }); + + // Default keybindings for this view + KeyBindings.Add (KeyCode.PageUp, Command.PageUp); + KeyBindings.Add (KeyCode.PageDown, Command.PageDown); + KeyBindings.Add (KeyCode.PageUp | KeyCode.ShiftMask, Command.PageUpExtend); + KeyBindings.Add (KeyCode.PageDown | KeyCode.ShiftMask, Command.PageDownExtend); + KeyBindings.Add (KeyCode.CursorRight, Command.Expand); + KeyBindings.Add (KeyCode.CursorRight | KeyCode.CtrlMask, Command.ExpandAll); + KeyBindings.Add (KeyCode.CursorLeft, Command.Collapse); + KeyBindings.Add (KeyCode.CursorLeft | KeyCode.CtrlMask, Command.CollapseAll); + + KeyBindings.Add (KeyCode.CursorUp, Command.LineUp); + KeyBindings.Add (KeyCode.CursorUp | KeyCode.ShiftMask, Command.LineUpExtend); + KeyBindings.Add (KeyCode.CursorUp | KeyCode.CtrlMask, Command.LineUpToFirstBranch); + + KeyBindings.Add (KeyCode.CursorDown, Command.LineDown); + KeyBindings.Add (KeyCode.CursorDown | KeyCode.ShiftMask, Command.LineDownExtend); + KeyBindings.Add (KeyCode.CursorDown | KeyCode.CtrlMask, Command.LineDownToLastBranch); + + KeyBindings.Add (KeyCode.Home, Command.TopHome); + KeyBindings.Add (KeyCode.End, Command.BottomEnd); + KeyBindings.Add (KeyCode.A | KeyCode.CtrlMask, Command.SelectAll); + KeyBindings.Add (ObjectActivationKey, Command.Accept); } /// - /// Hierarchical tree view with expandable branches. Branch objects are dynamically determined - /// when expanded using a user defined . - /// - /// See TreeView Deep Dive for more information. - /// - public class TreeView : View, ITreeView where T : class { - private int scrollOffsetVertical; - private int scrollOffsetHorizontal; - - /// - /// Determines how sub branches of the tree are dynamically built at runtime as the user - /// expands root nodes. - /// - /// - public ITreeBuilder TreeBuilder { get; set; } - - /// - /// private variable for - /// - T selectedObject; - - /// - /// Contains options for changing how the tree is rendered. - /// - public TreeStyle Style { get; set; } = new TreeStyle (); - - /// - /// True to allow multiple objects to be selected at once. - /// - /// - public bool MultiSelect { get; set; } = true; - - /// - /// Maximum number of nodes that can be expanded in any given branch. - /// - public int MaxDepth { get; set; } = 100; - - /// - /// True makes a letter key press navigate to the next visible branch that begins with - /// that letter/digit. - /// - /// - public bool AllowLetterBasedNavigation { get; set; } = true; - - /// - /// The currently selected object in the tree. When is true this - /// is the object at which the cursor is at. - /// - public T SelectedObject { - get => selectedObject; - set { - var oldValue = selectedObject; - selectedObject = value; - - if (!ReferenceEquals (oldValue, value)) { - OnSelectionChanged (new SelectionChangedEventArgs (this, oldValue, value)); - } - } - } + /// Initialises .Creates a new tree view with absolute + /// positioning. Use to set set root + /// objects for the tree. + /// + public TreeView (ITreeBuilder builder) : this () => TreeBuilder = builder; - /// - /// This event is raised when an object is activated e.g. by double clicking or - /// pressing . - /// - public event EventHandler> ObjectActivated; - - // TODO: Update to use Key instead of KeyCode - /// - /// Key which when pressed triggers . - /// Defaults to Enter. - /// - public KeyCode ObjectActivationKey { - get => objectActivationKey; - set { - if (objectActivationKey != value) { - KeyBindings.Replace (ObjectActivationKey, value); - objectActivationKey = value; - } - } - } + /// + /// Determines how sub branches of the tree are dynamically built at runtime as the user + /// expands root nodes. + /// + /// + public ITreeBuilder TreeBuilder { get; set; } - /// - /// Mouse event to trigger . - /// Defaults to double click (). - /// Set to null to disable this feature. - /// - /// - public MouseFlags? ObjectActivationButton { get; set; } = MouseFlags.Button1DoubleClicked; - - /// - /// Delegate for multi colored tree views. Return the to use - /// for each passed object or null to use the default. - /// - public Func ColorGetter { get; set; } - - /// - /// Secondary selected regions of tree when is true. - /// - private Stack> multiSelectedRegions = new Stack> (); - - /// - /// Cached result of - /// - private IReadOnlyCollection> cachedLineMap; - - /// - /// Error message to display when the control is not properly initialized at draw time - /// (nodes added but no tree builder set). - /// - public static string NoBuilderError = "ERROR: TreeBuilder Not Set"; - private KeyCode objectActivationKey = KeyCode.Enter; - - /// - /// Called when the changes. - /// - public event EventHandler> SelectionChanged; - - /// - /// Called once for each visible row during rendering. Can be used - /// to make last minute changes to color or text rendered - /// - public event EventHandler> DrawLine; - - /// - /// The root objects in the tree, note that this collection is of root objects only. - /// - public IEnumerable Objects { get => roots.Keys; } - - /// - /// Map of root objects to the branches under them. All objects have - /// a even if that branch has no children. - /// - internal Dictionary> roots { get; set; } = new Dictionary> (); - - /// - /// The amount of tree view that has been scrolled off the top of the screen (by the user - /// scrolling down). - /// - /// Setting a value of less than 0 will result in a offset of 0. To see changes - /// in the UI call . - public int ScrollOffsetVertical { - get => scrollOffsetVertical; - set { - scrollOffsetVertical = Math.Max (0, value); - } - } + /// + /// True to allow multiple objects to be selected at once. + /// + /// + public bool MultiSelect { get; set; } = true; + + /// + /// Maximum number of nodes that can be expanded in any given branch. + /// + public int MaxDepth { get; set; } = 100; + + /// + /// True makes a letter key press navigate to the next visible branch that begins with + /// that letter/digit. + /// + /// + public bool AllowLetterBasedNavigation { get; set; } = true; + + /// + /// The currently selected object in the tree. When is true this + /// is the object at which the cursor is at. + /// + public T SelectedObject { + get => selectedObject; + set { + var oldValue = selectedObject; + selectedObject = value; - /// - /// The amount of tree view that has been scrolled to the right (horizontally). - /// - /// Setting a value of less than 0 will result in a offset of 0. To see changes - /// in the UI call . - public int ScrollOffsetHorizontal { - get => scrollOffsetHorizontal; - set { - scrollOffsetHorizontal = Math.Max (0, value); + if (!ReferenceEquals (oldValue, value)) { + OnSelectionChanged (new SelectionChangedEventArgs (this, oldValue, value)); } } + } - /// - /// The current number of rows in the tree (ignoring the controls bounds). - /// - public int ContentHeight => BuildLineMap ().Count (); - - /// - /// Returns the string representation of model objects hosted in the tree. Default - /// implementation is to call . - /// - /// - public AspectGetterDelegate AspectGetter { get; set; } = (o) => o.ToString () ?? ""; - - CursorVisibility desiredCursorVisibility = CursorVisibility.Invisible; - - /// - /// Interface for filtering which lines of the tree are displayed - /// e.g. to provide text searching. Defaults to - /// (no filtering). - /// - public ITreeViewFilter Filter = null; - - /// - /// Get / Set the wished cursor when the tree is focused. - /// Only applies when is true. - /// Defaults to . - /// - public CursorVisibility DesiredCursorVisibility { - get { - return MultiSelect ? desiredCursorVisibility : CursorVisibility.Invisible; - } - set { - if (desiredCursorVisibility != value) { - desiredCursorVisibility = value; - if (HasFocus) { - Application.Driver.SetCursorVisibility (DesiredCursorVisibility); - } - } + // TODO: Update to use Key instead of KeyCode + /// + /// Key which when pressed triggers . + /// Defaults to Enter. + /// + public KeyCode ObjectActivationKey { + get => objectActivationKey; + set { + if (objectActivationKey != value) { + KeyBindings.Replace (ObjectActivationKey, value); + objectActivationKey = value; } } + } - /// - /// Creates a new tree view with absolute positioning. - /// Use to set set root objects for the tree. - /// Children will not be rendered until you set . - /// - public TreeView () : base () - { - CanFocus = true; - - // Things this view knows how to do - AddCommand (Command.PageUp, () => { MovePageUp (false); return true; }); - AddCommand (Command.PageDown, () => { MovePageDown (false); return true; }); - AddCommand (Command.PageUpExtend, () => { MovePageUp (true); return true; }); - AddCommand (Command.PageDownExtend, () => { MovePageDown (true); return true; }); - AddCommand (Command.Expand, () => { Expand (); return true; }); - AddCommand (Command.ExpandAll, () => { ExpandAll (SelectedObject); return true; }); - AddCommand (Command.Collapse, () => { CursorLeft (false); return true; }); - AddCommand (Command.CollapseAll, () => { CursorLeft (true); return true; }); - AddCommand (Command.LineUp, () => { AdjustSelection (-1, false); return true; }); - AddCommand (Command.LineUpExtend, () => { AdjustSelection (-1, true); return true; }); - AddCommand (Command.LineUpToFirstBranch, () => { AdjustSelectionToBranchStart (); return true; }); - - AddCommand (Command.LineDown, () => { AdjustSelection (1, false); return true; }); - AddCommand (Command.LineDownExtend, () => { AdjustSelection (1, true); return true; }); - AddCommand (Command.LineDownToLastBranch, () => { AdjustSelectionToBranchEnd (); return true; }); - - AddCommand (Command.TopHome, () => { GoToFirst (); return true; }); - AddCommand (Command.BottomEnd, () => { GoToEnd (); return true; }); - AddCommand (Command.SelectAll, () => { SelectAll (); return true; }); - - AddCommand (Command.ScrollUp, () => { ScrollUp (); return true; }); - AddCommand (Command.ScrollDown, () => { ScrollDown (); return true; }); - AddCommand (Command.Accept, () => { ActivateSelectedObjectIfAny (); return true; }); - - // Default keybindings for this view - KeyBindings.Add (KeyCode.PageUp, Command.PageUp); - KeyBindings.Add (KeyCode.PageDown, Command.PageDown); - KeyBindings.Add (KeyCode.PageUp | KeyCode.ShiftMask, Command.PageUpExtend); - KeyBindings.Add (KeyCode.PageDown | KeyCode.ShiftMask, Command.PageDownExtend); - KeyBindings.Add (KeyCode.CursorRight, Command.Expand); - KeyBindings.Add (KeyCode.CursorRight | KeyCode.CtrlMask, Command.ExpandAll); - KeyBindings.Add (KeyCode.CursorLeft, Command.Collapse); - KeyBindings.Add (KeyCode.CursorLeft | KeyCode.CtrlMask, Command.CollapseAll); - - KeyBindings.Add (KeyCode.CursorUp, Command.LineUp); - KeyBindings.Add (KeyCode.CursorUp | KeyCode.ShiftMask, Command.LineUpExtend); - KeyBindings.Add (KeyCode.CursorUp | KeyCode.CtrlMask, Command.LineUpToFirstBranch); - - KeyBindings.Add (KeyCode.CursorDown, Command.LineDown); - KeyBindings.Add (KeyCode.CursorDown | KeyCode.ShiftMask, Command.LineDownExtend); - KeyBindings.Add (KeyCode.CursorDown | KeyCode.CtrlMask, Command.LineDownToLastBranch); - - KeyBindings.Add (KeyCode.Home, Command.TopHome); - KeyBindings.Add (KeyCode.End, Command.BottomEnd); - KeyBindings.Add (KeyCode.A | KeyCode.CtrlMask, Command.SelectAll); - KeyBindings.Add (ObjectActivationKey, Command.Accept); - } - - /// - /// Initialises .Creates a new tree view with absolute - /// positioning. Use to set set root - /// objects for the tree. - /// - public TreeView (ITreeBuilder builder) : this () - { - TreeBuilder = builder; - } - - /// - public override bool OnEnter (View view) - { - Application.Driver.SetCursorVisibility (DesiredCursorVisibility); - - if (SelectedObject == null && Objects.Any ()) { - SelectedObject = Objects.First (); - } + /// + /// Mouse event to trigger . + /// Defaults to double click (). + /// Set to null to disable this feature. + /// + /// + public MouseFlags? ObjectActivationButton { get; set; } = MouseFlags.Button1DoubleClicked; - return base.OnEnter (view); - } + /// + /// Delegate for multi colored tree views. Return the to use + /// for each passed object or null to use the default. + /// + public Func ColorGetter { get; set; } - /// - /// Adds a new root level object unless it is already a root of the tree. - /// - /// - public void AddObject (T o) - { - if (!roots.ContainsKey (o)) { - roots.Add (o, new Branch (this, null, o)); - InvalidateLineMap (); - SetNeedsDisplay (); - } - } + /// + /// The root objects in the tree, note that this collection is of root objects only. + /// + public IEnumerable Objects => roots.Keys; - /// - /// Removes all objects from the tree and clears . - /// - public void ClearObjects () - { - SelectedObject = default (T); - multiSelectedRegions.Clear (); - roots = new Dictionary> (); - InvalidateLineMap (); - SetNeedsDisplay (); - } + /// + /// Map of root objects to the branches under them. All objects have + /// a even if that branch has no children. + /// + internal Dictionary> roots { get; set; } = new (); - /// - /// Removes the given root object from the tree - /// - /// If is the currently then the - /// selection is cleared. - /// - public void Remove (T o) - { - if (roots.ContainsKey (o)) { - roots.Remove (o); - InvalidateLineMap (); - SetNeedsDisplay (); + /// + /// The amount of tree view that has been scrolled off the top of the screen (by the user + /// scrolling down). + /// + /// + /// Setting a value of less than 0 will result in a offset of 0. To see changes + /// in the UI call . + /// + public int ScrollOffsetVertical { + get => scrollOffsetVertical; + set => scrollOffsetVertical = Math.Max (0, value); + } + + /// + /// The amount of tree view that has been scrolled to the right (horizontally). + /// + /// + /// Setting a value of less than 0 will result in a offset of 0. To see changes + /// in the UI call . + /// + public int ScrollOffsetHorizontal { + get => scrollOffsetHorizontal; + set => scrollOffsetHorizontal = Math.Max (0, value); + } + + /// + /// The current number of rows in the tree (ignoring the controls bounds). + /// + public int ContentHeight => BuildLineMap ().Count (); + + /// + /// Returns the string representation of model objects hosted in the tree. Default + /// implementation is to call . + /// + /// + public AspectGetterDelegate AspectGetter { get; set; } = o => o.ToString () ?? ""; - if (Equals (SelectedObject, o)) { - SelectedObject = default (T); + /// + /// Get / Set the wished cursor when the tree is focused. + /// Only applies when is true. + /// Defaults to . + /// + public CursorVisibility DesiredCursorVisibility { + get => MultiSelect ? desiredCursorVisibility : CursorVisibility.Invisible; + set { + if (desiredCursorVisibility != value) { + desiredCursorVisibility = value; + if (HasFocus) { + Application.Driver.SetCursorVisibility (DesiredCursorVisibility); } } } + } - /// - /// Adds many new root level objects. Objects that are already root objects are ignored. - /// - /// Objects to add as new root level objects..\ - public void AddObjects (IEnumerable collection) - { - bool objectsAdded = false; + /// + /// Gets the that searches the collection as + /// the user types. + /// + public CollectionNavigator KeystrokeNavigator { get; } = new (); - foreach (var o in collection) { - if (!roots.ContainsKey (o)) { - roots.Add (o, new Branch (this, null, o)); - objectsAdded = true; - } - } + /// + /// Contains options for changing how the tree is rendered. + /// + public TreeStyle Style { get; set; } = new (); - if (objectsAdded) { - InvalidateLineMap (); - SetNeedsDisplay (); - } - } + /// + /// Removes all objects from the tree and clears . + /// + public void ClearObjects () + { + SelectedObject = default; + multiSelectedRegions.Clear (); + roots = new Dictionary> (); + InvalidateLineMap (); + SetNeedsDisplay (); + } - /// - /// Refreshes the state of the object in the tree. This will - /// recompute children, string representation etc. - /// - /// This has no effect if the object is not exposed in the tree. - /// - /// True to also refresh all ancestors of the objects branch - /// (starting with the root). False to refresh only the passed node. - public void RefreshObject (T o, bool startAtTop = false) - { - var branch = ObjectToBranch (o); - if (branch != null) { - branch.Refresh (startAtTop); - InvalidateLineMap (); - SetNeedsDisplay (); - } + /// + /// This event is raised when an object is activated e.g. by double clicking or + /// pressing . + /// + public event EventHandler> ObjectActivated; + + /// + /// Called when the changes. + /// + public event EventHandler> SelectionChanged; + + /// + /// Called once for each visible row during rendering. Can be used + /// to make last minute changes to color or text rendered + /// + public event EventHandler> DrawLine; + + /// + public override bool OnEnter (View view) + { + Application.Driver.SetCursorVisibility (DesiredCursorVisibility); + if (SelectedObject == null && Objects.Any ()) { + SelectedObject = Objects.First (); } - /// - /// Rebuilds the tree structure for all exposed objects starting with the root objects. - /// Call this method when you know there are changes to the tree but don't know which - /// objects have changed (otherwise use ). - /// - public void RebuildTree () - { - foreach (var branch in roots.Values) { - branch.Rebuild (); - } + return base.OnEnter (view); + } + /// + /// Adds a new root level object unless it is already a root of the tree. + /// + /// + public void AddObject (T o) + { + if (!roots.ContainsKey (o)) { + roots.Add (o, new Branch (this, null, o)); InvalidateLineMap (); SetNeedsDisplay (); } + } - /// - /// Returns the currently expanded children of the passed object. Returns an empty - /// collection if the branch is not exposed or not expanded. - /// - /// An object in the tree. - /// - public IEnumerable GetChildren (T o) - { - var branch = ObjectToBranch (o); + /// + /// Removes the given root object from the tree + /// + /// + /// If is the currently then the + /// selection is cleared + /// + /// . + /// + public void Remove (T o) + { + if (roots.ContainsKey (o)) { + roots.Remove (o); + InvalidateLineMap (); + SetNeedsDisplay (); - if (branch == null || !branch.IsExpanded) { - return new T [0]; + if (Equals (SelectedObject, o)) { + SelectedObject = default; } - - return branch.ChildBranches?.Values?.Select (b => b.Model)?.ToArray () ?? new T [0]; - } - /// - /// Returns the parent object of in the tree. Returns null if - /// the object is not exposed in the tree. - /// - /// An object in the tree. - /// - public T GetParent (T o) - { - return ObjectToBranch (o)?.Parent?.Model; } + } - /// - public override void OnDrawContent (Rect contentArea) - { - if (roots == null) { - return; - } + /// + /// Adds many new root level objects. Objects that are already root objects are ignored. + /// + /// Objects to add as new root level objects. + /// .\ + public void AddObjects (IEnumerable collection) + { + var objectsAdded = false; - if (TreeBuilder == null) { - Move (0, 0); - Driver.AddStr (NoBuilderError); - return; + foreach (var o in collection) { + if (!roots.ContainsKey (o)) { + roots.Add (o, new Branch (this, null, o)); + objectsAdded = true; } + } - var map = BuildLineMap (); - - for (int line = 0; line < Bounds.Height; line++) { + if (objectsAdded) { + InvalidateLineMap (); + SetNeedsDisplay (); + } + } - var idxToRender = ScrollOffsetVertical + line; + /// + /// Refreshes the state of the object in the tree. This will + /// recompute children, string representation etc. + /// + /// This has no effect if the object is not exposed in the tree. + /// + /// + /// True to also refresh all ancestors of the objects branch + /// (starting with the root). False to refresh only the passed node. + /// + public void RefreshObject (T o, bool startAtTop = false) + { + var branch = ObjectToBranch (o); + if (branch != null) { + branch.Refresh (startAtTop); + InvalidateLineMap (); + SetNeedsDisplay (); + } - // Is there part of the tree view to render? - if (idxToRender < map.Count) { - // Render the line - map.ElementAt (idxToRender).Draw (Driver, ColorScheme, line, Bounds.Width); - } else { + } - // Else clear the line to prevent stale symbols due to scrolling etc - Move (0, line); - Driver.SetAttribute (GetNormalColor ()); - Driver.AddStr (new string (' ', Bounds.Width)); - } - } + /// + /// Rebuilds the tree structure for all exposed objects starting with the root objects. + /// Call this method when you know there are changes to the tree but don't know which + /// objects have changed (otherwise use ). + /// + public void RebuildTree () + { + foreach (var branch in roots.Values) { + branch.Rebuild (); } - /// - /// Returns the index of the object if it is currently exposed (it's - /// parent(s) have been expanded). This can be used with - /// and to scroll to a specific object. - /// - /// Uses the Equals method and returns the first index at which the object is found - /// or -1 if it is not found. - /// An object that appears in your tree and is currently exposed. - /// The index the object was found at or -1 if it is not currently revealed or - /// not in the tree at all. - public int GetScrollOffsetOf (T o) - { - var map = BuildLineMap (); - for (int i = 0; i < map.Count; i++) { - if (map.ElementAt (i).Model.Equals (o)) { - return i; - } - } + InvalidateLineMap (); + SetNeedsDisplay (); + } + + /// + /// Returns the currently expanded children of the passed object. Returns an empty + /// collection if the branch is not exposed or not expanded. + /// + /// An object in the tree. + /// + public IEnumerable GetChildren (T o) + { + var branch = ObjectToBranch (o); - //object not found - return -1; + if (branch == null || !branch.IsExpanded) { + return new T [0]; } - /// - /// Returns the maximum width line in the tree including prefix and expansion symbols. - /// - /// True to consider only rows currently visible (based on window - /// bounds and . False to calculate the width of - /// every exposed branch in the tree. - /// - public int GetContentWidth (bool visible) - { - var map = BuildLineMap (); + return branch.ChildBranches?.Values?.Select (b => b.Model)?.ToArray () ?? new T [0]; + } - if (map.Count == 0) { - return 0; - } + /// + /// Returns the parent object of in the tree. Returns null if + /// the object is not exposed in the tree. + /// + /// An object in the tree. + /// + public T GetParent (T o) => ObjectToBranch (o)?.Parent?.Model; - if (visible) { + /// + public override void OnDrawContent (Rect contentArea) + { + if (roots == null) { + return; + } - //Somehow we managed to scroll off the end of the control - if (ScrollOffsetVertical >= map.Count) { - return 0; - } + if (TreeBuilder == null) { + Move (0, 0); + Driver.AddStr (NoBuilderError); + return; + } - // If control has no height to it then there is no visible area for content - if (Bounds.Height == 0) { - return 0; - } + var map = BuildLineMap (); - return map.Skip (ScrollOffsetVertical).Take (Bounds.Height).Max (b => b.GetWidth (Driver)); + for (var line = 0; line < Bounds.Height; line++) { + + var idxToRender = ScrollOffsetVertical + line; + + // Is there part of the tree view to render? + if (idxToRender < map.Count) { + // Render the line + map.ElementAt (idxToRender).Draw (Driver, ColorScheme, line, Bounds.Width); } else { - return map.Max (b => b.GetWidth (Driver)); + // Else clear the line to prevent stale symbols due to scrolling etc + Move (0, line); + Driver.SetAttribute (GetNormalColor ()); + Driver.AddStr (new string (' ', Bounds.Width)); } } + } - /// - /// Calculates all currently visible/expanded branches (including leafs) and outputs them - /// by index from the top of the screen. - /// - /// Index 0 of the returned array is the first item that should be visible in the - /// top of the control, index 1 is the next etc. - /// - internal IReadOnlyCollection> BuildLineMap () - { - if (cachedLineMap != null) { - return cachedLineMap; - } + /// + /// Returns the index of the object if it is currently exposed (it's + /// parent(s) have been expanded). This can be used with + /// and to scroll to a specific object. + /// + /// + /// Uses the Equals method and returns the first index at which the object is found + /// or -1 if it is not found. + /// + /// An object that appears in your tree and is currently exposed. + /// + /// The index the object was found at or -1 if it is not currently revealed or + /// not in the tree at all. + /// + public int GetScrollOffsetOf (T o) + { + var map = BuildLineMap (); + for (var i = 0; i < map.Count; i++) { + if (map.ElementAt (i).Model.Equals (o)) { + return i; + } + } + + //object not found + return -1; + } + + /// + /// Returns the maximum width line in the tree including prefix and expansion symbols. + /// + /// + /// True to consider only rows currently visible (based on window + /// bounds and . False to calculate the width of + /// every exposed branch in the tree. + /// + /// + public int GetContentWidth (bool visible) + { + var map = BuildLineMap (); - List> toReturn = new List> (); + if (map.Count == 0) { + return 0; + } - foreach (var root in roots.Values) { + if (visible) { - var toAdd = AddToLineMap (root, false, out var isMatch); - if (isMatch) { - toReturn.AddRange (toAdd); - } + //Somehow we managed to scroll off the end of the control + if (ScrollOffsetVertical >= map.Count) { + return 0; } - cachedLineMap = new ReadOnlyCollection> (toReturn); + // If control has no height to it then there is no visible area for content + if (Bounds.Height == 0) { + return 0; + } - // Update the collection used for search-typing - KeystrokeNavigator.Collection = cachedLineMap.Select (b => AspectGetter (b.Model)).ToArray (); - return cachedLineMap; + return map.Skip (ScrollOffsetVertical).Take (Bounds.Height).Max (b => b.GetWidth (Driver)); } + return map.Max (b => b.GetWidth (Driver)); + } - private bool IsFilterMatch (Branch branch) - { - return Filter?.IsMatch (branch.Model) ?? true; + /// + /// Calculates all currently visible/expanded branches (including leafs) and outputs them + /// by index from the top of the screen. + /// + /// + /// Index 0 of the returned array is the first item that should be visible in the + /// top of the control, index 1 is the next etc. + /// + /// + internal IReadOnlyCollection> BuildLineMap () + { + if (cachedLineMap != null) { + return cachedLineMap; } - private IEnumerable> AddToLineMap (Branch currentBranch, bool parentMatches, out bool match) - { - bool weMatch = IsFilterMatch (currentBranch); - bool anyChildMatches = false; + var toReturn = new List> (); - var toReturn = new List> (); - var children = new List> (); + foreach (var root in roots.Values) { - if (currentBranch.IsExpanded) { - foreach (var subBranch in currentBranch.ChildBranches.Values) { + var toAdd = AddToLineMap (root, false, out var isMatch); + if (isMatch) { + toReturn.AddRange (toAdd); + } + } - foreach (var sub in AddToLineMap (subBranch, weMatch, out var childMatch)) { + cachedLineMap = new ReadOnlyCollection> (toReturn); - if (childMatch) { - children.Add (sub); - anyChildMatches = true; - } - } - } - } + // Update the collection used for search-typing + KeystrokeNavigator.Collection = cachedLineMap.Select (b => AspectGetter (b.Model)).ToArray (); + return cachedLineMap; + } - if (parentMatches || weMatch || anyChildMatches) { - match = true; - toReturn.Add (currentBranch); - } else { - match = false; - } + bool IsFilterMatch (Branch branch) => Filter?.IsMatch (branch.Model) ?? true; - toReturn.AddRange (children); - return toReturn; - } + IEnumerable> AddToLineMap (Branch currentBranch, bool parentMatches, out bool match) + { + var weMatch = IsFilterMatch (currentBranch); + var anyChildMatches = false; - /// - /// Gets the that searches the collection as - /// the user types. - /// - public CollectionNavigator KeystrokeNavigator { get; private set; } = new CollectionNavigator (); + var toReturn = new List> (); + var children = new List> (); - /// - public override bool OnProcessKeyDown (Key keyEvent) - { - if (!Enabled) { - return false; - } + if (currentBranch.IsExpanded) { + foreach (var subBranch in currentBranch.ChildBranches.Values) { + + foreach (var sub in AddToLineMap (subBranch, weMatch, out var childMatch)) { - try { - // BUGBUG: this should move to OnInvokingKeyBindings - // If not a keybinding, is the key a searchable key press? - if (CollectionNavigator.IsCompatibleKey (keyEvent) && AllowLetterBasedNavigation) { - IReadOnlyCollection> map; - - // If there has been a call to InvalidateMap since the last time - // we need a new one to reflect the new exposed tree state - map = BuildLineMap (); - - // Find the current selected object within the tree - var current = map.IndexOf (b => b.Model == SelectedObject); - var newIndex = KeystrokeNavigator?.GetNextMatchingItem (current, (char)keyEvent); - - if (newIndex is int && newIndex != -1) { - SelectedObject = map.ElementAt ((int)newIndex).Model; - EnsureVisible (selectedObject); - SetNeedsDisplay (); - return true; + if (childMatch) { + children.Add (sub); + anyChildMatches = true; } } - } finally { - if (IsInitialized) { - PositionCursor (); - } } + } + + if (parentMatches || weMatch || anyChildMatches) { + match = true; + toReturn.Add (currentBranch); + } else { + match = false; + } + toReturn.AddRange (children); + return toReturn; + } + + /// + public override bool OnProcessKeyDown (Key keyEvent) + { + if (!Enabled) { return false; } - /// - /// Triggers the event with the . - /// - /// This method also ensures that the selected object is visible. - /// - public void ActivateSelectedObjectIfAny () - { - var o = SelectedObject; + try { + // BUGBUG: this should move to OnInvokingKeyBindings + // If not a keybinding, is the key a searchable key press? + if (CollectionNavigatorBase.IsCompatibleKey (keyEvent) && AllowLetterBasedNavigation) { + IReadOnlyCollection> map; + + // If there has been a call to InvalidateMap since the last time + // we need a new one to reflect the new exposed tree state + map = BuildLineMap (); + + // Find the current selected object within the tree + var current = map.IndexOf (b => b.Model == SelectedObject); + var newIndex = KeystrokeNavigator?.GetNextMatchingItem (current, (char)keyEvent); - if (o != null) { - OnObjectActivated (new ObjectActivatedEventArgs (this, o)); + if (newIndex is int && newIndex != -1) { + SelectedObject = map.ElementAt ((int)newIndex).Model; + EnsureVisible (selectedObject); + SetNeedsDisplay (); + return true; + } + } + } finally { + if (IsInitialized) { PositionCursor (); } } - /// - /// - /// Returns the Y coordinate within the of the - /// tree at which would be displayed or null if - /// it is not currently exposed (e.g. its parent is collapsed). - /// - /// - /// Note that the returned value can be negative if the TreeView is scrolled - /// down and the object is off the top of the view. - /// - /// - /// - /// - public int? GetObjectRow (T toFind) - { - var idx = BuildLineMap ().IndexOf (o => o.Model.Equals (toFind)); - - if (idx == -1) - return null; - - return idx - ScrollOffsetVertical; - } - - /// - /// Moves the to the next item that begins with . - /// This method will loop back to the start of the tree if reaching the end without finding a match. - /// - /// The first character of the next item you want selected. - /// Case sensitivity of the search. - public void AdjustSelectionToNextItemBeginningWith (char character, StringComparison caseSensitivity = StringComparison.CurrentCultureIgnoreCase) - { - // search for next branch that begins with that letter - var characterAsStr = character.ToString (); - AdjustSelectionToNext (b => AspectGetter (b.Model).StartsWith (characterAsStr, caseSensitivity)); + return false; + } + + /// + /// Triggers the event with the . + /// + /// This method also ensures that the selected object is visible. + /// + public void ActivateSelectedObjectIfAny () + { + var o = SelectedObject; + if (o != null) { + OnObjectActivated (new ObjectActivatedEventArgs (this, o)); PositionCursor (); } + } - /// - /// Moves the selection up by the height of the control (1 page). - /// - /// True if the navigation should add the covered nodes to the selected current selection. - /// - public void MovePageUp (bool expandSelection = false) - { - AdjustSelection (-Bounds.Height, expandSelection); - } - - /// - /// Moves the selection down by the height of the control (1 page). - /// - /// True if the navigation should add the covered nodes to the selected current selection. - /// - public void MovePageDown (bool expandSelection = false) - { - AdjustSelection (Bounds.Height, expandSelection); - } - - /// - /// Scrolls the view area down a single line without changing the current selection. - /// - public void ScrollDown () - { - if (ScrollOffsetVertical <= ContentHeight - 2) { - ScrollOffsetVertical++; - SetNeedsDisplay (); - } + /// + /// + /// Returns the Y coordinate within the of the + /// tree at which would be displayed or null if + /// it is not currently exposed (e.g. its parent is collapsed). + /// + /// + /// Note that the returned value can be negative if the TreeView is scrolled + /// down and the object is off the top of the view. + /// + /// + /// + /// + public int? GetObjectRow (T toFind) + { + var idx = BuildLineMap ().IndexOf (o => o.Model.Equals (toFind)); + + if (idx == -1) { + return null; } - /// - /// Scrolls the view area up a single line without changing the current selection. - /// - public void ScrollUp () - { - if (scrollOffsetVertical > 0) { - ScrollOffsetVertical--; - SetNeedsDisplay (); - } + return idx - ScrollOffsetVertical; + } + + /// + /// Moves the to the next item that begins with . + /// This method will loop back to the start of the tree if reaching the end without finding a match. + /// + /// The first character of the next item you want selected. + /// Case sensitivity of the search. + public void AdjustSelectionToNextItemBeginningWith (char character, StringComparison caseSensitivity = StringComparison.CurrentCultureIgnoreCase) + { + // search for next branch that begins with that letter + var characterAsStr = character.ToString (); + AdjustSelectionToNext (b => AspectGetter (b.Model).StartsWith (characterAsStr, caseSensitivity)); + + PositionCursor (); + } + + /// + /// Moves the selection up by the height of the control (1 page). + /// + /// True if the navigation should add the covered nodes to the selected current selection. + /// + public void MovePageUp (bool expandSelection = false) => AdjustSelection (-Bounds.Height, expandSelection); + + /// + /// Moves the selection down by the height of the control (1 page). + /// + /// True if the navigation should add the covered nodes to the selected current selection. + /// + public void MovePageDown (bool expandSelection = false) => AdjustSelection (Bounds.Height, expandSelection); + + /// + /// Scrolls the view area down a single line without changing the current selection. + /// + public void ScrollDown () + { + if (ScrollOffsetVertical <= ContentHeight - 2) { + ScrollOffsetVertical++; + SetNeedsDisplay (); } + } - /// - /// Raises the event. - /// - /// - protected virtual void OnObjectActivated (ObjectActivatedEventArgs e) - { - ObjectActivated?.Invoke (this, e); - } - - /// - /// Returns the object in the tree list that is currently visible. - /// at the provided row. Returns null if no object is at that location. - /// - /// - /// If you have screen coordinates then use - /// to translate these into the client area of the . - /// - /// The row of the of the . - /// The object currently displayed on this row or null. - public T GetObjectOnRow (int row) - { - return HitTest (row)?.Model; - } - - /// - public override bool MouseEvent (MouseEvent me) - { - // If it is not an event we care about - if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) && - !me.Flags.HasFlag (ObjectActivationButton ?? MouseFlags.Button1DoubleClicked) && - !me.Flags.HasFlag (MouseFlags.WheeledDown) && - !me.Flags.HasFlag (MouseFlags.WheeledUp) && - !me.Flags.HasFlag (MouseFlags.WheeledRight) && - !me.Flags.HasFlag (MouseFlags.WheeledLeft)) { - - // do nothing - return false; - } + /// + /// Scrolls the view area up a single line without changing the current selection. + /// + public void ScrollUp () + { + if (scrollOffsetVertical > 0) { + ScrollOffsetVertical--; + SetNeedsDisplay (); + } + } - if (!HasFocus && CanFocus) { - SetFocus (); - } + /// + /// Raises the event. + /// + /// + protected virtual void OnObjectActivated (ObjectActivatedEventArgs e) => ObjectActivated?.Invoke (this, e); - if (me.Flags == MouseFlags.WheeledDown) { + /// + /// Returns the object in the tree list that is currently visible. + /// at the provided row. Returns null if no object is at that location. + /// + /// + /// If you have screen coordinates then use + /// to translate these into the client area of the . + /// + /// The row of the of the . + /// The object currently displayed on this row or null. + public T GetObjectOnRow (int row) => HitTest (row)?.Model; + + /// + public override bool MouseEvent (MouseEvent me) + { + // If it is not an event we care about + if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) && + !me.Flags.HasFlag (ObjectActivationButton ?? MouseFlags.Button1DoubleClicked) && + !me.Flags.HasFlag (MouseFlags.WheeledDown) && + !me.Flags.HasFlag (MouseFlags.WheeledUp) && + !me.Flags.HasFlag (MouseFlags.WheeledRight) && + !me.Flags.HasFlag (MouseFlags.WheeledLeft)) { + + // do nothing + return false; + } - ScrollDown (); + if (!HasFocus && CanFocus) { + SetFocus (); + } - return true; - } else if (me.Flags == MouseFlags.WheeledUp) { - ScrollUp (); + if (me.Flags == MouseFlags.WheeledDown) { - return true; - } + ScrollDown (); - if (me.Flags == MouseFlags.WheeledRight) { + return true; + } + if (me.Flags == MouseFlags.WheeledUp) { + ScrollUp (); - ScrollOffsetHorizontal++; - SetNeedsDisplay (); + return true; + } - return true; - } else if (me.Flags == MouseFlags.WheeledLeft) { - ScrollOffsetHorizontal--; - SetNeedsDisplay (); + if (me.Flags == MouseFlags.WheeledRight) { - return true; - } + ScrollOffsetHorizontal++; + SetNeedsDisplay (); + + return true; + } + if (me.Flags == MouseFlags.WheeledLeft) { + ScrollOffsetHorizontal--; + SetNeedsDisplay (); - if (me.Flags.HasFlag (MouseFlags.Button1Clicked)) { + return true; + } - // The line they clicked on a branch - var clickedBranch = HitTest (me.Y); + if (me.Flags.HasFlag (MouseFlags.Button1Clicked)) { - if (clickedBranch == null) { - return false; - } + // The line they clicked on a branch + var clickedBranch = HitTest (me.Y); - bool isExpandToggleAttempt = clickedBranch.IsHitOnExpandableSymbol (Driver, me.X); + if (clickedBranch == null) { + return false; + } - // If we are already selected (double click) - if (Equals (SelectedObject, clickedBranch.Model)) { - isExpandToggleAttempt = true; - } + var isExpandToggleAttempt = clickedBranch.IsHitOnExpandableSymbol (Driver, me.X); - // if they clicked on the +/- expansion symbol - if (isExpandToggleAttempt) { + // If we are already selected (double click) + if (Equals (SelectedObject, clickedBranch.Model)) { + isExpandToggleAttempt = true; + } - if (clickedBranch.IsExpanded) { - clickedBranch.Collapse (); - InvalidateLineMap (); - } else - if (clickedBranch.CanExpand ()) { - clickedBranch.Expand (); - InvalidateLineMap (); - } else { - SelectedObject = clickedBranch.Model; // It is a leaf node - multiSelectedRegions.Clear (); - } + // if they clicked on the +/- expansion symbol + if (isExpandToggleAttempt) { + + if (clickedBranch.IsExpanded) { + clickedBranch.Collapse (); + InvalidateLineMap (); + } else if (clickedBranch.CanExpand ()) { + clickedBranch.Expand (); + InvalidateLineMap (); } else { - // It is a first click somewhere in the current line that doesn't look like an expansion/collapse attempt - SelectedObject = clickedBranch.Model; + SelectedObject = clickedBranch.Model; // It is a leaf node multiSelectedRegions.Clear (); } - - SetNeedsDisplay (); - return true; + } else { + // It is a first click somewhere in the current line that doesn't look like an expansion/collapse attempt + SelectedObject = clickedBranch.Model; + multiSelectedRegions.Clear (); } - // If it is activation via mouse (e.g. double click) - if (ObjectActivationButton.HasValue && me.Flags.HasFlag (ObjectActivationButton.Value)) { - // The line they clicked on a branch - var clickedBranch = HitTest (me.Y); + SetNeedsDisplay (); + return true; + } + + // If it is activation via mouse (e.g. double click) + if (ObjectActivationButton.HasValue && me.Flags.HasFlag (ObjectActivationButton.Value)) { + // The line they clicked on a branch + var clickedBranch = HitTest (me.Y); - if (clickedBranch == null) { - return false; - } + if (clickedBranch == null) { + return false; + } - // Double click changes the selection to the clicked node as well as triggering - // activation otherwise it feels wierd - SelectedObject = clickedBranch.Model; - SetNeedsDisplay (); + // Double click changes the selection to the clicked node as well as triggering + // activation otherwise it feels wierd + SelectedObject = clickedBranch.Model; + SetNeedsDisplay (); - // trigger activation event - OnObjectActivated (new ObjectActivatedEventArgs (this, clickedBranch.Model)); + // trigger activation event + OnObjectActivated (new ObjectActivatedEventArgs (this, clickedBranch.Model)); - // mouse event is handled. - return true; - } - return false; + // mouse event is handled. + return true; } + return false; + } - /// - /// Returns the branch at the given client - /// coordinate e.g. following a click event. - /// - /// Client Y position in the controls bounds. - /// The clicked branch or null if outside of tree region. - private Branch HitTest (int y) - { - var map = BuildLineMap (); - - var idx = y + ScrollOffsetVertical; + /// + /// Returns the branch at the given client + /// coordinate e.g. following a click event. + /// + /// Client Y position in the controls bounds. + /// The clicked branch or null if outside of tree region. + Branch HitTest (int y) + { + var map = BuildLineMap (); - // click is outside any visible nodes - if (idx < 0 || idx >= map.Count) { - return null; - } + var idx = y + ScrollOffsetVertical; - // The line they clicked on - return map.ElementAt (idx); + // click is outside any visible nodes + if (idx < 0 || idx >= map.Count) { + return null; } - /// - /// Positions the cursor at the start of the selected objects line (if visible). - /// - public override void PositionCursor () - { - if (CanFocus && HasFocus && Visible && SelectedObject != null) { + // The line they clicked on + return map.ElementAt (idx); + } - var map = BuildLineMap (); - var idx = map.IndexOf (b => b.Model.Equals (SelectedObject)); + /// + /// Positions the cursor at the start of the selected objects line (if visible). + /// + public override void PositionCursor () + { + if (CanFocus && HasFocus && Visible && SelectedObject != null) { - // if currently selected line is visible - if (idx - ScrollOffsetVertical >= 0 && idx - ScrollOffsetVertical < Bounds.Height) { - Move (0, idx - ScrollOffsetVertical); - } else { - base.PositionCursor (); - } + var map = BuildLineMap (); + var idx = map.IndexOf (b => b.Model.Equals (SelectedObject)); + // if currently selected line is visible + if (idx - ScrollOffsetVertical >= 0 && idx - ScrollOffsetVertical < Bounds.Height) { + Move (0, idx - ScrollOffsetVertical); } else { base.PositionCursor (); } + + } else { + base.PositionCursor (); } + } - /// - /// Determines systems behaviour when the left arrow key is pressed. Default behaviour is - /// to collapse the current tree node if possible otherwise changes selection to current - /// branches parent. - /// - protected virtual void CursorLeft (bool ctrl) - { - if (IsExpanded (SelectedObject)) { + /// + /// Determines systems behaviour when the left arrow key is pressed. Default behaviour is + /// to collapse the current tree node if possible otherwise changes selection to current + /// branches parent. + /// + protected virtual void CursorLeft (bool ctrl) + { + if (IsExpanded (SelectedObject)) { - if (ctrl) { - CollapseAll (SelectedObject); - } else { - Collapse (SelectedObject); - } + if (ctrl) { + CollapseAll (SelectedObject); } else { - var parent = GetParent (SelectedObject); + Collapse (SelectedObject); + } + } else { + var parent = GetParent (SelectedObject); - if (parent != null) { - SelectedObject = parent; - AdjustSelection (0); - SetNeedsDisplay (); - } + if (parent != null) { + SelectedObject = parent; + AdjustSelection (0); + SetNeedsDisplay (); } } + } - /// - /// Changes the to the first root object and resets - /// the to 0. - /// - public void GoToFirst () - { - ScrollOffsetVertical = 0; - SelectedObject = roots.Keys.FirstOrDefault (); + /// + /// Changes the to the first root object and resets + /// the to 0. + /// + public void GoToFirst () + { + ScrollOffsetVertical = 0; + SelectedObject = roots.Keys.FirstOrDefault (); - SetNeedsDisplay (); - } + SetNeedsDisplay (); + } - /// - /// Changes the to the last object in the tree and scrolls so - /// that it is visible. - /// - public void GoToEnd () - { - var map = BuildLineMap (); - ScrollOffsetVertical = Math.Max (0, map.Count - Bounds.Height + 1); - SelectedObject = map.LastOrDefault ()?.Model; + /// + /// Changes the to the last object in the tree and scrolls so + /// that it is visible. + /// + public void GoToEnd () + { + var map = BuildLineMap (); + ScrollOffsetVertical = Math.Max (0, map.Count - Bounds.Height + 1); + SelectedObject = map.LastOrDefault ()?.Model; - SetNeedsDisplay (); + SetNeedsDisplay (); + } + + /// + /// Changes the to and scrolls to ensure + /// it is visible. Has no effect if is not exposed in the tree (e.g. + /// its parents are collapsed). + /// + /// + public void GoTo (T toSelect) + { + if (ObjectToBranch (toSelect) == null) { + return; } - /// - /// Changes the to and scrolls to ensure - /// it is visible. Has no effect if is not exposed in the tree (e.g. - /// its parents are collapsed). - /// - /// - public void GoTo (T toSelect) - { - if (ObjectToBranch (toSelect) == null) { - return; - } + SelectedObject = toSelect; + EnsureVisible (toSelect); + SetNeedsDisplay (); + } - SelectedObject = toSelect; - EnsureVisible (toSelect); - SetNeedsDisplay (); + /// + /// The number of screen lines to move the currently selected object by. Supports negative values. + /// . Each branch occupies 1 line on screen. + /// + /// + /// If nothing is currently selected or the selected object is no longer in the tree + /// then the first object in the tree is selected instead. + /// + /// Positive to move the selection down the screen, negative to move it up + /// + /// True to expand the selection (assuming + /// is enabled). False to replace. + /// + public void AdjustSelection (int offset, bool expandSelection = false) + { + // if it is not a shift click or we don't allow multi select + if (!expandSelection || !MultiSelect) { + multiSelectedRegions.Clear (); } - /// - /// The number of screen lines to move the currently selected object by. Supports negative values. - /// . Each branch occupies 1 line on screen. - /// - /// If nothing is currently selected or the selected object is no longer in the tree - /// then the first object in the tree is selected instead. - /// Positive to move the selection down the screen, negative to move it up - /// True to expand the selection (assuming - /// is enabled). False to replace. - public void AdjustSelection (int offset, bool expandSelection = false) - { - // if it is not a shift click or we don't allow multi select - if (!expandSelection || !MultiSelect) { - multiSelectedRegions.Clear (); - } + if (SelectedObject == null) { + SelectedObject = roots.Keys.FirstOrDefault (); + } else { + var map = BuildLineMap (); - if (SelectedObject == null) { + var idx = map.IndexOf (b => b.Model.Equals (SelectedObject)); + + if (idx == -1) { + // The current selection has disapeared! SelectedObject = roots.Keys.FirstOrDefault (); } else { - var map = BuildLineMap (); + var newIdx = Math.Min (Math.Max (0, idx + offset), map.Count - 1); - var idx = map.IndexOf (b => b.Model.Equals (SelectedObject)); + var newBranch = map.ElementAt (newIdx); - if (idx == -1) { - // The current selection has disapeared! - SelectedObject = roots.Keys.FirstOrDefault (); - } else { - var newIdx = Math.Min (Math.Max (0, idx + offset), map.Count - 1); - - var newBranch = map.ElementAt (newIdx); - - // If it is a multi selection - if (expandSelection && MultiSelect) { - if (multiSelectedRegions.Any ()) { - // expand the existing head selection - var head = multiSelectedRegions.Pop (); - multiSelectedRegions.Push (new TreeSelection (head.Origin, newIdx, map)); - } else { - // or start a new multi selection region - multiSelectedRegions.Push (new TreeSelection (map.ElementAt (idx), newIdx, map)); - } + // If it is a multi selection + if (expandSelection && MultiSelect) { + if (multiSelectedRegions.Any ()) { + // expand the existing head selection + var head = multiSelectedRegions.Pop (); + multiSelectedRegions.Push (new TreeSelection (head.Origin, newIdx, map)); + } else { + // or start a new multi selection region + multiSelectedRegions.Push (new TreeSelection (map.ElementAt (idx), newIdx, map)); } + } - SelectedObject = newBranch.Model; + SelectedObject = newBranch.Model; - EnsureVisible (SelectedObject); - } + EnsureVisible (SelectedObject); } - SetNeedsDisplay (); } + SetNeedsDisplay (); + } - /// - /// Moves the selection to the first child in the currently selected level. - /// - public void AdjustSelectionToBranchStart () - { - var o = SelectedObject; - if (o == null) { - return; - } - - var map = BuildLineMap (); + /// + /// Moves the selection to the first child in the currently selected level. + /// + public void AdjustSelectionToBranchStart () + { + var o = SelectedObject; + if (o == null) { + return; + } - int currentIdx = map.IndexOf (b => Equals (b.Model, o)); + var map = BuildLineMap (); - if (currentIdx == -1) { - return; - } + var currentIdx = map.IndexOf (b => Equals (b.Model, o)); - var currentBranch = map.ElementAt (currentIdx); - var next = currentBranch; + if (currentIdx == -1) { + return; + } - for (; currentIdx >= 0; currentIdx--) { - //if it is the beginning of the current depth of branch - if (currentBranch.Depth != next.Depth) { + var currentBranch = map.ElementAt (currentIdx); + var next = currentBranch; - SelectedObject = currentBranch.Model; - EnsureVisible (currentBranch.Model); - SetNeedsDisplay (); - return; - } + for (; currentIdx >= 0; currentIdx--) { + //if it is the beginning of the current depth of branch + if (currentBranch.Depth != next.Depth) { - // look at next branch up for consideration - currentBranch = next; - next = map.ElementAt (currentIdx); + SelectedObject = currentBranch.Model; + EnsureVisible (currentBranch.Model); + SetNeedsDisplay (); + return; } - // We ran all the way to top of tree - GoToFirst (); + // look at next branch up for consideration + currentBranch = next; + next = map.ElementAt (currentIdx); } - /// - /// Moves the selection to the last child in the currently selected level. - /// - public void AdjustSelectionToBranchEnd () - { - var o = SelectedObject; - if (o == null) { - return; - } + // We ran all the way to top of tree + GoToFirst (); + } - var map = BuildLineMap (); + /// + /// Moves the selection to the last child in the currently selected level. + /// + public void AdjustSelectionToBranchEnd () + { + var o = SelectedObject; + if (o == null) { + return; + } - int currentIdx = map.IndexOf (b => Equals (b.Model, o)); + var map = BuildLineMap (); - if (currentIdx == -1) { - return; - } + var currentIdx = map.IndexOf (b => Equals (b.Model, o)); - var currentBranch = map.ElementAt (currentIdx); - var next = currentBranch; + if (currentIdx == -1) { + return; + } - for (; currentIdx < map.Count; currentIdx++) { - //if it is the end of the current depth of branch - if (currentBranch.Depth != next.Depth) { + var currentBranch = map.ElementAt (currentIdx); + var next = currentBranch; - SelectedObject = currentBranch.Model; - EnsureVisible (currentBranch.Model); - SetNeedsDisplay (); - return; - } + for (; currentIdx < map.Count; currentIdx++) { + //if it is the end of the current depth of branch + if (currentBranch.Depth != next.Depth) { - // look at next branch for consideration - currentBranch = next; - next = map.ElementAt (currentIdx); + SelectedObject = currentBranch.Model; + EnsureVisible (currentBranch.Model); + SetNeedsDisplay (); + return; } - GoToEnd (); + + // look at next branch for consideration + currentBranch = next; + next = map.ElementAt (currentIdx); } + GoToEnd (); + } - /// - /// Sets the selection to the next branch that matches the . - /// - /// - private void AdjustSelectionToNext (Func, bool> predicate) - { - var map = BuildLineMap (); + /// + /// Sets the selection to the next branch that matches the . + /// + /// + void AdjustSelectionToNext (Func, bool> predicate) + { + var map = BuildLineMap (); - // empty map means we can't select anything anyway - if (map.Count == 0) { - return; - } + // empty map means we can't select anything anyway + if (map.Count == 0) { + return; + } - // Start searching from the first element in the map - var idxStart = 0; + // Start searching from the first element in the map + var idxStart = 0; - // or the current selected branch - if (SelectedObject != null) { - idxStart = map.IndexOf (b => Equals (b.Model, SelectedObject)); - } + // or the current selected branch + if (SelectedObject != null) { + idxStart = map.IndexOf (b => Equals (b.Model, SelectedObject)); + } - // if currently selected object mysteriously vanished, search from beginning - if (idxStart == -1) { - idxStart = 0; - } + // if currently selected object mysteriously vanished, search from beginning + if (idxStart == -1) { + idxStart = 0; + } - // loop around all indexes and back to first index - for (int idxCur = (idxStart + 1) % map.Count; idxCur != idxStart; idxCur = (idxCur + 1) % map.Count) { - if (predicate (map.ElementAt (idxCur))) { - SelectedObject = map.ElementAt (idxCur).Model; - EnsureVisible (map.ElementAt (idxCur).Model); - SetNeedsDisplay (); - return; - } + // loop around all indexes and back to first index + for (var idxCur = (idxStart + 1) % map.Count; idxCur != idxStart; idxCur = (idxCur + 1) % map.Count) { + if (predicate (map.ElementAt (idxCur))) { + SelectedObject = map.ElementAt (idxCur).Model; + EnsureVisible (map.ElementAt (idxCur).Model); + SetNeedsDisplay (); + return; } } + } - /// - /// Adjusts the to ensure the given - /// is visible. Has no effect if already visible. - /// - public void EnsureVisible (T model) - { - var map = BuildLineMap (); + /// + /// Adjusts the to ensure the given + /// is visible. Has no effect if already visible. + /// + public void EnsureVisible (T model) + { + var map = BuildLineMap (); - var idx = map.IndexOf (b => Equals (b.Model, model)); + var idx = map.IndexOf (b => Equals (b.Model, model)); - if (idx == -1) { - return; - } + if (idx == -1) { + return; + } - /*this -1 allows for possible horizontal scroll bar in the last row of the control*/ - int leaveSpace = Style.LeaveLastRow ? 1 : 0; + /*this -1 allows for possible horizontal scroll bar in the last row of the control*/ + var leaveSpace = Style.LeaveLastRow ? 1 : 0; - if (idx < ScrollOffsetVertical) { - //if user has scrolled up too far to see their selection - ScrollOffsetVertical = idx; - } else if (idx >= ScrollOffsetVertical + Bounds.Height - leaveSpace) { + if (idx < ScrollOffsetVertical) { + //if user has scrolled up too far to see their selection + ScrollOffsetVertical = idx; + } else if (idx >= ScrollOffsetVertical + Bounds.Height - leaveSpace) { - //if user has scrolled off bottom of visible tree - ScrollOffsetVertical = Math.Max (0, (idx + 1) - (Bounds.Height - leaveSpace)); - } + //if user has scrolled off bottom of visible tree + ScrollOffsetVertical = Math.Max (0, idx + 1 - (Bounds.Height - leaveSpace)); } + } + + /// + /// Expands the currently . + /// + public void Expand () => Expand (SelectedObject); - /// - /// Expands the currently . - /// - public void Expand () - { - Expand (SelectedObject); + /// + /// Expands the supplied object if it is contained in the tree (either as a root object or + /// as an exposed branch object). + /// + /// The object to expand. + public void Expand (T toExpand) + { + if (toExpand == null) { + return; } - /// - /// Expands the supplied object if it is contained in the tree (either as a root object or - /// as an exposed branch object). - /// - /// The object to expand. - public void Expand (T toExpand) - { - if (toExpand == null) { - return; - } + ObjectToBranch (toExpand)?.Expand (); + InvalidateLineMap (); + SetNeedsDisplay (); + } - ObjectToBranch (toExpand)?.Expand (); - InvalidateLineMap (); - SetNeedsDisplay (); + /// + /// Expands the supplied object and all child objects. + /// + /// The object to expand. + public void ExpandAll (T toExpand) + { + if (toExpand == null) { + return; } - /// - /// Expands the supplied object and all child objects. - /// - /// The object to expand. - public void ExpandAll (T toExpand) - { - if (toExpand == null) { - return; - } + ObjectToBranch (toExpand)?.ExpandAll (); + InvalidateLineMap (); + SetNeedsDisplay (); + } - ObjectToBranch (toExpand)?.ExpandAll (); - InvalidateLineMap (); - SetNeedsDisplay (); + /// + /// Fully expands all nodes in the tree, if the tree is very big and built dynamically this + /// may take a while (e.g. for file system). + /// + public void ExpandAll () + { + foreach (var item in roots) { + item.Value.ExpandAll (); } - /// - /// Fully expands all nodes in the tree, if the tree is very big and built dynamically this - /// may take a while (e.g. for file system). - /// - public void ExpandAll () - { - foreach (var item in roots) { - item.Value.ExpandAll (); - } - InvalidateLineMap (); - SetNeedsDisplay (); - } - /// - /// Returns true if the given object is exposed in the tree and can be - /// expanded otherwise false. - /// - /// - /// - public bool CanExpand (T o) - { - return ObjectToBranch (o)?.CanExpand () ?? false; - } - - /// - /// Returns true if the given object is exposed in the tree and - /// expanded otherwise false. - /// - /// - /// - public bool IsExpanded (T o) - { - return ObjectToBranch (o)?.IsExpanded ?? false; - } - - /// - /// Collapses the - /// - public void Collapse () - { - Collapse (selectedObject); - } - - /// - /// Collapses the supplied object if it is currently expanded . - /// - /// The object to collapse. - public void Collapse (T toCollapse) - { - CollapseImpl (toCollapse, false); - } - - /// - /// Collapses the supplied object if it is currently expanded. Also collapses all children - /// branches (this will only become apparent when/if the user expands it again). - /// - /// The object to collapse. - public void CollapseAll (T toCollapse) - { - CollapseImpl (toCollapse, true); - } - - /// - /// Collapses all root nodes in the tree. - /// - public void CollapseAll () - { - foreach (var item in roots) { - item.Value.Collapse (); - } + InvalidateLineMap (); + SetNeedsDisplay (); + } - InvalidateLineMap (); - SetNeedsDisplay (); - } + /// + /// Returns true if the given object is exposed in the tree and can be + /// expanded otherwise false. + /// + /// + /// + public bool CanExpand (T o) => ObjectToBranch (o)?.CanExpand () ?? false; - /// - /// Implementation of and . Performs - /// operation and updates selection if disapeared. - /// - /// - /// - protected void CollapseImpl (T toCollapse, bool all) - { - if (toCollapse == null) { - return; - } + /// + /// Returns true if the given object is exposed in the tree and + /// expanded otherwise false. + /// + /// + /// + public bool IsExpanded (T o) => ObjectToBranch (o)?.IsExpanded ?? false; - var branch = ObjectToBranch (toCollapse); + /// + /// Collapses the + /// + public void Collapse () => Collapse (selectedObject); - // Nothing to collapse - if (branch == null) { - return; - } + /// + /// Collapses the supplied object if it is currently expanded . + /// + /// The object to collapse. + public void Collapse (T toCollapse) => CollapseImpl (toCollapse, false); - if (all) { - branch.CollapseAll (); - } else { - branch.Collapse (); - } + /// + /// Collapses the supplied object if it is currently expanded. Also collapses all children + /// branches (this will only become apparent when/if the user expands it again). + /// + /// The object to collapse. + public void CollapseAll (T toCollapse) => CollapseImpl (toCollapse, true); - if (SelectedObject != null && ObjectToBranch (SelectedObject) == null) { - // If the old selection suddenly became invalid then clear it - SelectedObject = null; - } + /// + /// Collapses all root nodes in the tree. + /// + public void CollapseAll () + { + foreach (var item in roots) { + item.Value.Collapse (); + } - InvalidateLineMap (); - SetNeedsDisplay (); + InvalidateLineMap (); + SetNeedsDisplay (); + } + + /// + /// Implementation of and . Performs + /// operation and updates selection if disapeared. + /// + /// + /// + protected void CollapseImpl (T toCollapse, bool all) + { + if (toCollapse == null) { + return; } - /// - /// Clears any cached results of the tree state. - /// - public void InvalidateLineMap () - { - cachedLineMap = null; - } - - /// - /// Returns the corresponding in the tree for - /// . This will not work for objects hidden - /// by their parent being collapsed. - /// - /// - /// The branch for or null if it is not currently - /// exposed in the tree. - private Branch ObjectToBranch (T toFind) - { - return BuildLineMap ().FirstOrDefault (o => o.Model.Equals (toFind)); - } - - /// - /// Returns true if the is either the - /// or part of a . - /// - /// - /// - public bool IsSelected (T model) - { - return Equals (SelectedObject, model) || - (MultiSelect && multiSelectedRegions.Any (s => s.Contains (model))); - } - - /// - /// Returns (if not null) and all multi selected objects if - /// is true - /// - /// - public IEnumerable GetAllSelectedObjects () - { - var map = BuildLineMap (); + var branch = ObjectToBranch (toCollapse); - // To determine multi selected objects, start with the line map, that avoids yielding - // hidden nodes that were selected then the parent collapsed e.g. programmatically or - // with mouse click - if (MultiSelect) { - foreach (var m in map.Select (b => b.Model).Where (IsSelected)) { - yield return m; - } - } else { - if (SelectedObject != null) { - yield return SelectedObject; - } - } + // Nothing to collapse + if (branch == null) { + return; } - /// - /// Selects all objects in the tree when is enabled otherwise - /// does nothing. - /// - public void SelectAll () - { - if (!MultiSelect) { - return; - } + if (all) { + branch.CollapseAll (); + } else { + branch.Collapse (); + } - multiSelectedRegions.Clear (); + if (SelectedObject != null && ObjectToBranch (SelectedObject) == null) { + // If the old selection suddenly became invalid then clear it + SelectedObject = null; + } - var map = BuildLineMap (); + InvalidateLineMap (); + SetNeedsDisplay (); + } - if (map.Count == 0) { - return; - } + /// + /// Clears any cached results of the tree state. + /// + public void InvalidateLineMap () => cachedLineMap = null; - multiSelectedRegions.Push (new TreeSelection (map.ElementAt (0), map.Count, map)); - SetNeedsDisplay (); + /// + /// Returns the corresponding in the tree for + /// . This will not work for objects hidden + /// by their parent being collapsed. + /// + /// + /// + /// The branch for or null if it is not currently + /// exposed in the tree. + /// + Branch ObjectToBranch (T toFind) => BuildLineMap ().FirstOrDefault (o => o.Model.Equals (toFind)); - OnSelectionChanged (new SelectionChangedEventArgs (this, SelectedObject, SelectedObject)); - } + /// + /// Returns true if the is either the + /// or part of a . + /// + /// + /// + public bool IsSelected (T model) => Equals (SelectedObject, model) || + MultiSelect && multiSelectedRegions.Any (s => s.Contains (model)); - /// - /// Raises the SelectionChanged event. - /// - /// - protected virtual void OnSelectionChanged (SelectionChangedEventArgs e) - { - SelectionChanged?.Invoke (this, e); + /// + /// Returns (if not null) and all multi selected objects if + /// is true + /// + /// + public IEnumerable GetAllSelectedObjects () + { + var map = BuildLineMap (); + + // To determine multi selected objects, start with the line map, that avoids yielding + // hidden nodes that were selected then the parent collapsed e.g. programmatically or + // with mouse click + if (MultiSelect) { + foreach (var m in map.Select (b => b.Model).Where (IsSelected)) { + yield return m; + } + } else { + if (SelectedObject != null) { + yield return SelectedObject; + } } + } - /// - /// Raises the DrawLine event - /// - /// - internal void OnDrawLine (DrawTreeViewLineEventArgs e) - { - DrawLine?.Invoke (this, e); + /// + /// Selects all objects in the tree when is enabled otherwise + /// does nothing. + /// + public void SelectAll () + { + if (!MultiSelect) { + return; } - /// - protected override void Dispose (bool disposing) - { - base.Dispose (disposing); + multiSelectedRegions.Clear (); - ColorGetter = null; + var map = BuildLineMap (); + + if (map.Count == 0) { + return; } + + multiSelectedRegions.Push (new TreeSelection (map.ElementAt (0), map.Count, map)); + SetNeedsDisplay (); + + OnSelectionChanged (new SelectionChangedEventArgs (this, SelectedObject, SelectedObject)); } - class TreeSelection where T : class { - public Branch Origin { get; } + /// + /// Raises the SelectionChanged event. + /// + /// + protected virtual void OnSelectionChanged (SelectionChangedEventArgs e) => SelectionChanged?.Invoke (this, e); - private HashSet included = new HashSet (); + /// + /// Raises the DrawLine event + /// + /// + internal void OnDrawLine (DrawTreeViewLineEventArgs e) => DrawLine?.Invoke (this, e); - /// - /// Creates a new selection between two branches in the tree - /// - /// - /// - /// - public TreeSelection (Branch from, int toIndex, IReadOnlyCollection> map) - { - Origin = from; - included.Add (Origin.Model); + /// + protected override void Dispose (bool disposing) + { + base.Dispose (disposing); - var oldIdx = map.IndexOf (from); + ColorGetter = null; + } +} - var lowIndex = Math.Min (oldIdx, toIndex); - var highIndex = Math.Max (oldIdx, toIndex); +class TreeSelection where T : class { - // Select everything between the old and new indexes - foreach (var alsoInclude in map.Skip (lowIndex).Take (highIndex - lowIndex)) { - included.Add (alsoInclude.Model); - } + readonly HashSet included = new (); + /// + /// Creates a new selection between two branches in the tree + /// + /// + /// + /// + public TreeSelection (Branch from, int toIndex, IReadOnlyCollection> map) + { + Origin = from; + included.Add (Origin.Model); + + var oldIdx = map.IndexOf (from); + + var lowIndex = Math.Min (oldIdx, toIndex); + var highIndex = Math.Max (oldIdx, toIndex); + + // Select everything between the old and new indexes + foreach (var alsoInclude in map.Skip (lowIndex).Take (highIndex - lowIndex)) { + included.Add (alsoInclude.Model); } - public bool Contains (T model) - { - return included.Contains (model); - } + } + + public Branch Origin { get; } + + public bool Contains (T model) => included.Contains (model); } \ No newline at end of file diff --git a/UICatalog/Scenarios/TreeViewFileSystem.cs b/UICatalog/Scenarios/TreeViewFileSystem.cs index 91600a8cd7..62545fa382 100644 --- a/UICatalog/Scenarios/TreeViewFileSystem.cs +++ b/UICatalog/Scenarios/TreeViewFileSystem.cs @@ -364,20 +364,21 @@ void SetMultiSelect () void SetCustomColors () { - var hidden = new ColorScheme { - Focus = new Attribute (Color.BrightRed, _treeViewFiles.ColorScheme.Focus.Background), - Normal = new Attribute (Color.BrightYellow, _treeViewFiles.ColorScheme.Normal.Background) - }; - _miCustomColors.Checked = !_miCustomColors.Checked; if (_miCustomColors.Checked == true) { _treeViewFiles.ColorGetter = (m) => { if (m is IDirectoryInfo && m.Attributes.HasFlag (FileAttributes.Hidden)) { - return hidden; + return new ColorScheme { + Focus = new Attribute (Color.BrightRed, _treeViewFiles.ColorScheme.Focus.Background), + Normal = new Attribute (Color.BrightYellow, _treeViewFiles.ColorScheme.Normal.Background) + }; ; } if (m is IFileInfo && m.Attributes.HasFlag (FileAttributes.Hidden)) { - return hidden; + return new ColorScheme { + Focus = new Attribute (Color.BrightRed, _treeViewFiles.ColorScheme.Focus.Background), + Normal = new Attribute (Color.BrightYellow, _treeViewFiles.ColorScheme.Normal.Background) + }; ; } return null; }; diff --git a/UnitTests/Application/ApplicationTests.cs b/UnitTests/Application/ApplicationTests.cs index 5dbc3a9c09..00c9ae1722 100644 --- a/UnitTests/Application/ApplicationTests.cs +++ b/UnitTests/Application/ApplicationTests.cs @@ -566,7 +566,7 @@ public void Shutdown_Resets_SyncContext () } #endregion - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void Begin_Sets_Application_Top_To_Console_Size () { Assert.Equal (new Rect (0, 0, 80, 25), Application.Top.Frame); diff --git a/UnitTests/Configuration/ConfigurationMangerTests.cs b/UnitTests/Configuration/ConfigurationMangerTests.cs index 5c9ed616e6..a2ec1e6a95 100644 --- a/UnitTests/Configuration/ConfigurationMangerTests.cs +++ b/UnitTests/Configuration/ConfigurationMangerTests.cs @@ -297,7 +297,7 @@ public void TestConfigPropertyOmitClassName () Assert.Equal (pi, Themes ["Default"] ["ColorSchemes"].PropertyInfo); } - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void TestConfigurationManagerToJson () { Reset (); @@ -518,7 +518,7 @@ public void TestConfigurationManagerUpdateFromJson () Assert.Equal (new Color (Color.Blue), Colors.ColorSchemes ["Base"].Normal.Background); } - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void TestConfigurationManagerInvalidJsonThrows () { ThrowOnJsonErrors = true; @@ -690,7 +690,7 @@ public void TestConfigurationManagerInvalidJsonLogs () ThrowOnJsonErrors = false; } - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void LoadConfigurationFromAllSources_ShouldLoadSettingsFromAllSources () { //var _configFilename = "config.json"; diff --git a/UnitTests/Configuration/JsonConverterTests.cs b/UnitTests/Configuration/JsonConverterTests.cs index bc3f46924c..8fb9423f22 100644 --- a/UnitTests/Configuration/JsonConverterTests.cs +++ b/UnitTests/Configuration/JsonConverterTests.cs @@ -191,7 +191,7 @@ public void TestDeserialize () Assert.Equal (Color.BrightGreen, attribute.Background.ColorName); } - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void TestSerialize () { // Test serializing to human-readable color names @@ -229,7 +229,7 @@ public class ColorSchemeJsonConverterTests { // } // } // }"; - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void TestColorSchemesSerialization () { // Arrange diff --git a/UnitTests/Configuration/SettingsScopeTests.cs b/UnitTests/Configuration/SettingsScopeTests.cs index 09dac60243..c64d715c26 100644 --- a/UnitTests/Configuration/SettingsScopeTests.cs +++ b/UnitTests/Configuration/SettingsScopeTests.cs @@ -34,7 +34,7 @@ public void GetHardCodedDefaults_ShouldSetProperties () } - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void Apply_ShouldApplyProperties () { // arrange @@ -58,7 +58,7 @@ public void Apply_ShouldApplyProperties () Assert.True (Application.IsMouseDisabled); } - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void CopyUpdatedPropertiesFrom_ShouldCopyChangedPropertiesOnly () { Settings ["Application.QuitKey"].PropertyValue = new Key (KeyCode.End); diff --git a/UnitTests/Drawing/AttributeTests.cs b/UnitTests/Drawing/AttributeTests.cs index 96d091fc74..5fbda28e8f 100644 --- a/UnitTests/Drawing/AttributeTests.cs +++ b/UnitTests/Drawing/AttributeTests.cs @@ -1,14 +1,17 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Terminal.Gui; using Xunit; - // Alias Console to MockConsole so we don't accidentally use Console using Console = Terminal.Gui.FakeConsole; namespace Terminal.Gui.DrawingTests; + public class AttributeTests { + + [Fact] + public void Attribute_Is_Value_Type () => + // prove that Color is a value type + Assert.True (typeof (Attribute).IsValueType); + + [Fact] public void DefaultConstructor () { @@ -47,7 +50,8 @@ public void ColorNamesConstructor () Assert.Equal (new Color (Color.Blue), attribute.Background); } - [Fact, AutoInitShutdown] + [Fact] + [AutoInitShutdown] public void ColorConstructor () { // Arrange & Act @@ -60,7 +64,8 @@ public void ColorConstructor () Assert.Equal (backgroundColor, attribute.Background); } - [Fact, AutoInitShutdown] + [Fact] + [AutoInitShutdown] public void ColorAndColorNamesConstructor () { // Arrange & Act @@ -207,7 +212,7 @@ public void Implicit_Assign () // Test conversion to int attr = new Attribute (value, fg, bg); - int value_implicit = attr.PlatformColor; + var value_implicit = attr.PlatformColor; Assert.Equal (value, value_implicit); Assert.Equal (value, attr.PlatformColor); @@ -368,83 +373,4 @@ public void ToString_ShouldReturnFormattedStringWithForegroundAndBackground () // Assert Assert.Equal (expectedString, attributeString); } - - [Fact] - public void Changing_One_Default_Reference_Also_Change_All_References_But_Not_A_Instance_Reference () - { - // Make two local attributes, and grab Attribute.Default, which is a reference to a static. - Attribute attr1 = Attribute.Default; - Attribute attr2 = Attribute.Default; - // Make one local attributes, and grab Attribute(), which is a reference to a singleton. - Attribute attr3 = new Attribute (); // instance - - // Assert the starting state that is expected - Assert.Equal (ColorName.White, attr1.Foreground.ColorName); - Assert.Equal (ColorName.White, attr2.Foreground.ColorName); - Assert.Equal (ColorName.White, Attribute.Default.Foreground.ColorName); - Assert.Equal (ColorName.White, attr3.Foreground.ColorName); - - // Now set Foreground.ColorName to ColorName.Blue on one of our local attributes - attr1.Foreground.ColorName = ColorName.Blue; - - // Assert the newly-expected case - // The last two assertions will fail, because we have actually modified a singleton - Assert.Equal (ColorName.Blue, attr1.Foreground.ColorName); - Assert.Equal (ColorName.Blue, attr2.Foreground.ColorName); - Assert.Equal (ColorName.Blue, Attribute.Default.Foreground.ColorName); - Assert.Equal (ColorName.White, attr3.Foreground.ColorName); - - // Now set Foreground.ColorName to ColorName.Red on the singleton of our local attributes - attr3.Foreground.ColorName = ColorName.Red; - - // Assert the newly-expected case - // The assertions will not fail, because we have actually modified a singleton - Assert.Equal (ColorName.Blue, attr1.Foreground.ColorName); - Assert.Equal (ColorName.Blue, attr2.Foreground.ColorName); - Assert.Equal (ColorName.Blue, Attribute.Default.Foreground.ColorName); - Assert.Equal (ColorName.Red, attr3.Foreground.ColorName); - - // Now set Foreground.ColorName to ColorName.White on the static of our local attributes - // This also avoids errors on others unit test when the default is changed - Attribute.Default.Foreground.ColorName = ColorName.White; - - // Assert the newly-expected case - // The assertions will not fail, because we have actually modified the static default reference - Assert.Equal (ColorName.White, attr1.Foreground.ColorName); - Assert.Equal (ColorName.White, attr2.Foreground.ColorName); - Assert.Equal (ColorName.White, Attribute.Default.Foreground.ColorName); - Assert.Equal (ColorName.Red, attr3.Foreground.ColorName); - } - - [Fact] - public void Changing_One_Instance_Reference_Does_Not_Change_All_Instance_References () - { - // Make two local attributes, and grab Attribute (), which are a reference to a singleton. - Attribute attr1 = new Attribute (); - // Make two local attributes, and grab Attribute (Int), which are a reference to a singleton. - Attribute attr2 = new Attribute (-1); - - // Assert the starting state that is expected - Assert.Equal (ColorName.White, attr1.Foreground.ColorName); - Assert.Equal (ColorName.White, attr2.Foreground.ColorName); - Assert.Equal (ColorName.White, Attribute.Default.Foreground.ColorName); - - // Now set Foreground.ColorName to ColorName.Blue on one of our local attributes - attr1.Foreground.ColorName = ColorName.Blue; - - // Assert the newly-expected case - // The assertions will not fail, because we have actually modified a singleton - Assert.Equal (ColorName.Blue, attr1.Foreground.ColorName); - Assert.Equal (ColorName.White, attr2.Foreground.ColorName); - Assert.Equal (ColorName.White, Attribute.Default.Foreground.ColorName); - - // Now set Foreground.ColorName to ColorName.Red on the other singleton of our local attributes - attr2.Foreground.ColorName = ColorName.Red; - - // Assert the newly-expected case - // The assertions will not fail, because we have actually modified a singleton - Assert.Equal (ColorName.Blue, attr1.Foreground.ColorName); - Assert.Equal (ColorName.Red, attr2.Foreground.ColorName); - Assert.Equal (ColorName.White, Attribute.Default.Foreground.ColorName); - } -} +} \ No newline at end of file diff --git a/UnitTests/Drawing/ColorTests.cs b/UnitTests/Drawing/ColorTests.cs index c0b3fb60ee..8070f5fab2 100644 --- a/UnitTests/Drawing/ColorTests.cs +++ b/UnitTests/Drawing/ColorTests.cs @@ -1,18 +1,27 @@ -using Terminal.Gui; -using System; -using System.Buffers; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Globalization; +using System; using System.Linq; -using System.Text; -using System.Text.Json; using Xunit; -using static Unix.Terminal.Curses; + namespace Terminal.Gui.DrawingTests; public class ColorTests { + [Fact] + public void Color_Is_Value_Type () => + // prove that Color is a value type + Assert.True (typeof (Color).IsValueType); + + [Fact] + public void Colors_ColorSchemes_Property_Has_Private_Setter () + { + // Resharper Code Cleanup likes to remove the `private set; ` + // from the ColorSchemes property. This test will fail if + // that happens. + var property = typeof (Colors).GetProperty ("ColorSchemes"); + Assert.NotNull (property); + Assert.NotNull (property.SetMethod); + Assert.True (property.GetSetMethod (true).IsPrivate); + + } [Fact, AutoInitShutdown] public void ColorScheme_New () @@ -26,10 +35,10 @@ public void ColorScheme_New () [Fact] public void TestAllColors () { - var colorNames = Enum.GetValues (typeof (ColorName)).Cast ().Distinct ().ToList(); - Attribute [] attrs = new Attribute [colorNames.Count]; + var colorNames = Enum.GetValues (typeof (ColorName)).Cast ().Distinct ().ToList (); + var attrs = new Attribute [colorNames.Count]; - int idx = 0; + var idx = 0; foreach (ColorName bg in colorNames) { attrs [idx] = new Attribute (bg, colorNames.Count - 1 - bg); idx++; @@ -52,12 +61,9 @@ public void TestAllColors () Assert.Equal (new Attribute (Color.BrightYellow, Color.Blue), attrs [14]); Assert.Equal (new Attribute (Color.White, Color.Black), attrs [^1]); } - + [Fact] - public void ColorNames_HasOnly16DistinctElements () - { - Assert.Equal (16, Enum.GetValues (typeof (ColorName)).Cast ().Distinct ().Count ()); - } + public void ColorNames_HasOnly16DistinctElements () => Assert.Equal (16, Enum.GetValues (typeof (ColorName)).Cast ().Distinct ().Count ()); [Fact] public void ColorNames_HaveCorrectOrdinals () @@ -84,9 +90,9 @@ public void ColorNames_HaveCorrectOrdinals () public void Color_Constructor_WithRGBValues () { // Arrange - int expectedR = 255; - int expectedG = 0; - int expectedB = 128; + var expectedR = 255; + var expectedG = 0; + var expectedB = 128; // Act var color = new Color (expectedR, expectedG, expectedB); @@ -102,10 +108,10 @@ public void Color_Constructor_WithRGBValues () public void Color_Constructor_WithAlphaAndRGBValues () { // Arrange - int expectedA = 128; - int expectedR = 255; - int expectedG = 0; - int expectedB = 128; + var expectedA = 128; + var expectedR = 255; + var expectedG = 0; + var expectedB = 128; // Act var color = new Color (expectedR, expectedG, expectedB, expectedA); @@ -121,7 +127,7 @@ public void Color_Constructor_WithAlphaAndRGBValues () public void Color_Constructor_WithRgbaValue () { // Arrange - int expectedRgba = unchecked((int)0xFF804040); // R: 128, G: 64, B: 64, Alpha: 255 + var expectedRgba = unchecked((int)0xFF804040); // R: 128, G: 64, B: 64, Alpha: 255 // Act var color = new Color (expectedRgba); @@ -137,7 +143,7 @@ public void Color_Constructor_WithRgbaValue () public void Color_Constructor_WithColorName () { // Arrange - ColorName colorName = ColorName.Blue; + var colorName = ColorName.Blue; var expectedColor = new Color (0, 55, 218); // Blue // Act @@ -151,10 +157,10 @@ public void Color_Constructor_WithColorName () public void Color_ToString_WithNamedColor () { // Arrange - Color color = new Color (0, 55, 218); // Blue + var color = new Color (0, 55, 218); // Blue // Act - string colorString = color.ToString (); + var colorString = color.ToString (); // Assert Assert.Equal ("Blue", colorString); @@ -164,10 +170,10 @@ public void Color_ToString_WithNamedColor () public void Color_ToString_WithRGBColor () { // Arrange - Color color = new Color (1, 64, 32); // Custom RGB color + var color = new Color (1, 64, 32); // Custom RGB color // Act - string colorString = color.ToString (); + var colorString = color.ToString (); // Assert Assert.Equal ("#014020", colorString); @@ -177,7 +183,7 @@ public void Color_ToString_WithRGBColor () public void Color_ImplicitOperator_FromInt () { // Arrange - int Rgba = unchecked((int)0xFF804020); // R: 128, G: 64, B: 32, Alpha: 255 + var Rgba = unchecked((int)0xFF804020); // R: 128, G: 64, B: 32, Alpha: 255 var expectedColor = new Color (128, 64, 32); // Act @@ -192,10 +198,10 @@ public void Color_ExplicitOperator_ToInt () { // Arrange var color = new Color (128, 64, 32); - int expectedRgba = unchecked((int)0xFF804020); // R: 128, G: 64, B: 32, Alpha: 255 + var expectedRgba = unchecked((int)0xFF804020); // R: 128, G: 64, B: 32, Alpha: 255 // Act - int Rgba = (int)color; + var Rgba = (int)color; // Assert Assert.Equal (expectedRgba, Rgba); @@ -206,11 +212,11 @@ public void Color_ExplicitOperator_ToInt () public void Color_ImplicitOperator_FromColorNames () { // Arrange - ColorName colorName = ColorName.Blue; + var colorName = ColorName.Blue; var expectedColor = new Color (0, 55, 218); // Blue // Act - Color color = new Color (colorName); + var color = new Color (colorName); // Assert Assert.Equal (expectedColor, color); @@ -221,10 +227,10 @@ public void Color_ExplicitOperator_ToColorNames () { // Arrange var color = new Color (0, 0, 0x80); // Blue - ColorName expectedColorName = ColorName.Blue; + var expectedColorName = ColorName.Blue; // Act - ColorName colorName = (ColorName)color; + var colorName = (ColorName)color; // Assert Assert.Equal (expectedColorName, colorName); @@ -321,15 +327,14 @@ public void Color_ColorName_Get_ReturnsClosestColorName () public void FindClosestColor_ReturnsClosestColor () { // Test cases with RGB values and expected closest color names - var testCases = new [] - { - (new Color(0, 0, 0), ColorName.Black), - (new Color(255, 255, 255), ColorName.White), - (new Color(5, 100, 255), ColorName.BrightBlue), - (new Color(0, 255, 0), ColorName.BrightGreen), - (new Color(255, 70, 8), ColorName.BrightRed), - (new Color(0, 128, 128), ColorName.Cyan), - (new Color(128, 64, 32), ColorName.Yellow), + var testCases = new [] { + (new Color (0, 0, 0), ColorName.Black), + (new Color (255, 255, 255), ColorName.White), + (new Color (5, 100, 255), ColorName.BrightBlue), + (new Color (0, 255, 0), ColorName.BrightGreen), + (new Color (255, 70, 8), ColorName.BrightRed), + (new Color (0, 128, 128), ColorName.Cyan), + (new Color (128, 64, 32), ColorName.Yellow) }; foreach (var testCase in testCases) { @@ -341,20 +346,4 @@ public void FindClosestColor_ReturnsClosestColor () Assert.Equal (expectedColorName, actualColorName); } } - - [Fact] - public void Color_ColorName_Set_SetsColorBasedOnColorName () - { - // Arrange - var color = new Color (0, 0, 0); // Black - var expectedColor = new Color (ColorName.Magenta); - - // Act - color.ColorName = ColorName.Magenta; - - // Assert - Assert.Equal (expectedColor, color); - } -} - - +} \ No newline at end of file diff --git a/UnitTests/View/ViewTests.cs b/UnitTests/View/ViewTests.cs index 0573b250fa..64820150a9 100644 --- a/UnitTests/View/ViewTests.cs +++ b/UnitTests/View/ViewTests.cs @@ -479,7 +479,7 @@ public void LabelChangeText_RendersCorrectly_Constructors (int choice) } } - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void Internal_Tests () { Assert.Equal (new [] { View.Direction.Forward, View.Direction.Backward }, @@ -569,7 +569,7 @@ public void Internal_Tests () Application.End (runState); } - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void Visible_Sets_Also_Sets_Subviews () { var button = new Button ("Click Me"); @@ -637,7 +637,7 @@ int RunesCount () } } - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void GetTopSuperView_Test () { var v1 = new View (); @@ -673,7 +673,7 @@ public void GetTopSuperView_Test () top2.Dispose (); } - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void Clear_Can_Use_Driver_AddRune_Or_AddStr_Methods () { var view = new FrameView { @@ -721,7 +721,7 @@ public void Clear_Can_Use_Driver_AddRune_Or_AddStr_Methods () Assert.Equal (Rect.Empty, pos); } - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void Clear_Bounds_Can_Use_Driver_AddRune_Or_AddStr_Methods () { var view = new FrameView { @@ -784,7 +784,7 @@ public void IsAdded_Added_Removed () view.Dispose (); } - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void Visible_Clear_The_View_Output () { var view = new View ("Testing visibility."); // use View, not Label to avoid AutoSize == true @@ -820,7 +820,7 @@ public void Visible_Clear_The_View_Output () Application.End (rs); } - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void DrawContentComplete_Event_Is_Always_Called () { var viewCalled = false; @@ -838,7 +838,7 @@ public void DrawContentComplete_Event_Is_Always_Called () Assert.True (tvCalled); } - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void GetNormalColor_ColorScheme () { var view = new View { ColorScheme = Colors.Base }; @@ -850,7 +850,7 @@ public void GetNormalColor_ColorScheme () view.Dispose (); } - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void GetHotNormalColor_ColorScheme () { var view = new View { ColorScheme = Colors.Base }; @@ -926,7 +926,7 @@ public void Clear_Does_Not_Spillover_Its_Parent (bool label) Application.End (runState); } - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void Correct_Redraw_Bounds_NeedDisplay_On_Shrink_And_Move_Up_Left_Using_Frame () { var label = new Label ("At 0,0"); @@ -960,7 +960,7 @@ A text with some long width Application.End (runState); } - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void Correct_Redraw_Bounds_NeedDisplay_On_Shrink_And_Move_Up_Left_Using_Pos_Dim () { var label = new Label ("At 0,0"); @@ -996,7 +996,7 @@ A text with some long width Application.End (runState); } - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void Incorrect_Redraw_Bounds_NeedDisplay_On_Shrink_And_Move_Up_Left_Using_Frame () { var label = new Label ("At 0,0"); @@ -1032,7 +1032,7 @@ A text with some long width Application.End (runState); } - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void Incorrect_Redraw_Bounds_NeedDisplay_On_Shrink_And_Move_Up_Left_Using_Pos_Dim () { var label = new Label ("At 0,0"); @@ -1070,7 +1070,7 @@ A text with some long width Application.End (runState); } - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void Correct_Redraw_Bounds_NeedDisplay_On_Shrink_And_Move_Down_Right_Using_Frame () { var label = new Label ("At 0,0"); @@ -1105,7 +1105,7 @@ A text with some long width Application.End (runState); } - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void Correct_Redraw_Bounds_NeedDisplay_On_Shrink_And_Move_Down_Right_Using_Pos_Dim () { var label = new Label ("At 0,0"); @@ -1143,7 +1143,7 @@ A text with some long width Application.End (runState); } - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void Incorrect_Redraw_Bounds_NeedDisplay_On_Shrink_And_Move_Down_Right_Using_Frame () { var label = new Label ("At 0,0"); @@ -1177,7 +1177,7 @@ A text with some long width Application.End (runState); } - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void Incorrect_Redraw_Bounds_NeedDisplay_On_Shrink_And_Move_Down_Right_Using_Pos_Dim () { var label = new Label ("At 0,0"); @@ -1215,7 +1215,7 @@ A text with some long width Application.End (runState); } - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void Test_Nested_Views_With_Height_Equal_To_One () { var v = new View { Width = 11, Height = 3, ColorScheme = new ColorScheme () }; @@ -1245,7 +1245,7 @@ public void Test_Nested_Views_With_Height_Equal_To_One () bottom.Dispose (); } - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void Frame_Set_After_Initialize_Update_NeededDisplay () { var frame = new FrameView (); diff --git a/UnitTests/Views/OverlappedTests.cs b/UnitTests/Views/OverlappedTests.cs index efb482586b..c2ecedbda2 100644 --- a/UnitTests/Views/OverlappedTests.cs +++ b/UnitTests/Views/OverlappedTests.cs @@ -47,7 +47,7 @@ public void Dispose_Toplevel_IsOverlappedContainer_True_With_Begin () Application.Shutdown (); } - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void Application_RequestStop_With_Params_On_A_Not_OverlappedContainer_Always_Use_Application_Current () { var top1 = new Toplevel (); @@ -683,7 +683,7 @@ public void AllChildClosed_Event_Test () [Fact] public void MoveToOverlappedChild_Throw_NullReferenceException_Passing_Null_Parameter () => Assert.Throws (delegate { Application.MoveToOverlappedChild (null); }); - [Fact] [AutoInitShutdown] + [Fact, AutoInitShutdown] public void Visible_False_Does_Not_Clear () { var overlapped = new Overlapped (); diff --git a/UnitTests/Views/TreeViewTests.cs b/UnitTests/Views/TreeViewTests.cs index 3e3747ed04..bfce38cf6e 100644 --- a/UnitTests/Views/TreeViewTests.cs +++ b/UnitTests/Views/TreeViewTests.cs @@ -1,1286 +1,1286 @@ using System.Collections.Generic; using System.Linq; +using System.Text; using Xunit; using Xunit.Abstractions; -namespace Terminal.Gui.ViewsTests { +namespace Terminal.Gui.ViewsTests; - public class TreeViewTests { +public class TreeViewTests { - readonly ITestOutputHelper output; + readonly ITestOutputHelper _output; - public TreeViewTests (ITestOutputHelper output) - { - this.output = output; - } + public TreeViewTests (ITestOutputHelper output) => _output = output; - #region Test Setup Methods - class Factory { - public Car [] Cars { get; set; } - public override string ToString () - { - return "Factory"; - } - }; - class Car { - public string Name { get; set; } - public override string ToString () - { - return Name; - } - }; + /// + /// Tests that and are consistent + /// + [Fact] + public void IsExpanded_TrueAfterExpand () + { + var tree = CreateTree (out var f, out _, out _); + Assert.False (tree.IsExpanded (f)); + + tree.Expand (f); + Assert.True (tree.IsExpanded (f)); + + tree.Collapse (f); + Assert.False (tree.IsExpanded (f)); + } + + [Fact] + public void EmptyTreeView_ContentSizes () + { + var emptyTree = new TreeView (); + Assert.Equal (0, emptyTree.ContentHeight); + Assert.Equal (0, emptyTree.GetContentWidth (true)); + Assert.Equal (0, emptyTree.GetContentWidth (false)); + } + + [Fact] + public void EmptyTreeViewGeneric_ContentSizes () + { + var emptyTree = new TreeView (); + Assert.Equal (0, emptyTree.ContentHeight); + Assert.Equal (0, emptyTree.GetContentWidth (true)); + Assert.Equal (0, emptyTree.GetContentWidth (false)); + } + + /// + /// Tests that results in a correct content height + /// + [Fact] + public void ContentHeight_BiggerAfterExpand () + { + var tree = CreateTree (out var f, out _, out _); + Assert.Equal (1, tree.ContentHeight); + + tree.Expand (f); + Assert.Equal (3, tree.ContentHeight); + + tree.Collapse (f); + Assert.Equal (1, tree.ContentHeight); + } + + [Fact] + public void ContentWidth_BiggerAfterExpand () + { + var tree = CreateTree (out var f, out var car1, out _); + tree.BeginInit (); + tree.EndInit (); + + tree.Bounds = new Rect (0, 0, 10, 10); + + InitFakeDriver (); + + //-+Factory + Assert.Equal (9, tree.GetContentWidth (true)); + + car1.Name = "123456789"; + + tree.Expand (f); + + //..├-123456789 + Assert.Equal (13, tree.GetContentWidth (true)); + + tree.Collapse (f); + //-+Factory + Assert.Equal (9, tree.GetContentWidth (true)); + + Application.Shutdown (); + } + + [Fact] + public void ContentWidth_VisibleVsAll () + { + var tree = CreateTree (out var f, out var car1, out var car2); + tree.BeginInit (); + tree.EndInit (); + + // control only allows 1 row to be viewed at once + tree.Bounds = new Rect (0, 0, 20, 1); + + InitFakeDriver (); + + //-+Factory + Assert.Equal (9, tree.GetContentWidth (true)); + Assert.Equal (9, tree.GetContentWidth (false)); + + car1.Name = "123456789"; + car2.Name = "12345678"; + + tree.Expand (f); + + // Although expanded the bigger (longer) child node is not in the rendered area of the control + Assert.Equal (9, tree.GetContentWidth (true)); + Assert.Equal (13, tree.GetContentWidth (false)); // If you ask for the global max width it includes the longer child + + // Now that we have scrolled down 1 row we should see the big child + tree.ScrollOffsetVertical = 1; + Assert.Equal (13, tree.GetContentWidth (true)); + Assert.Equal (13, tree.GetContentWidth (false)); + + // Scroll down so only car2 is visible + tree.ScrollOffsetVertical = 2; + Assert.Equal (12, tree.GetContentWidth (true)); + Assert.Equal (13, tree.GetContentWidth (false)); + + // Scroll way down (off bottom of control even) + tree.ScrollOffsetVertical = 5; + Assert.Equal (0, tree.GetContentWidth (true)); + Assert.Equal (13, tree.GetContentWidth (false)); + + Application.Shutdown (); + } + + /// + /// Tests that and behaves correctly when + /// an object cannot be expanded (because it has no children) + /// + [Fact] + public void IsExpanded_FalseIfCannotExpand () + { + var tree = CreateTree (out var f, out var c, out _); + + // expose the car by expanding the factory + tree.Expand (f); + + // car is not expanded + Assert.False (tree.IsExpanded (c)); + + //try to expand the car (should have no effect because cars have no children) + tree.Expand (c); + + Assert.False (tree.IsExpanded (c)); - private TreeView CreateTree () - { - return CreateTree (out _, out _, out _); - } - - private TreeView CreateTree (out Factory factory1, out Car car1, out Car car2) - { - car1 = new Car (); - car2 = new Car (); - - factory1 = new Factory () { - Cars = new [] { car1, car2 } - }; - - var tree = new TreeView (new DelegateTreeBuilder ((s) => s is Factory f ? f.Cars : null)); - tree.AddObject (factory1); - - return tree; - } - #endregion - - /// - /// Tests that and are consistent - /// - [Fact] - public void IsExpanded_TrueAfterExpand () - { - var tree = CreateTree (out Factory f, out _, out _); - Assert.False (tree.IsExpanded (f)); - - tree.Expand (f); - Assert.True (tree.IsExpanded (f)); - - tree.Collapse (f); - Assert.False (tree.IsExpanded (f)); - } - - [Fact] - public void EmptyTreeView_ContentSizes () - { - var emptyTree = new TreeView (); - Assert.Equal (0, emptyTree.ContentHeight); - Assert.Equal (0, emptyTree.GetContentWidth (true)); - Assert.Equal (0, emptyTree.GetContentWidth (false)); - } - [Fact] - public void EmptyTreeViewGeneric_ContentSizes () - { - var emptyTree = new TreeView (); - Assert.Equal (0, emptyTree.ContentHeight); - Assert.Equal (0, emptyTree.GetContentWidth (true)); - Assert.Equal (0, emptyTree.GetContentWidth (false)); - } - - /// - /// Tests that results in a correct content height - /// - [Fact] - public void ContentHeight_BiggerAfterExpand () - { - var tree = CreateTree (out Factory f, out _, out _); - Assert.Equal (1, tree.ContentHeight); - - tree.Expand (f); - Assert.Equal (3, tree.ContentHeight); - - tree.Collapse (f); - Assert.Equal (1, tree.ContentHeight); - } - - [Fact] - public void ContentWidth_BiggerAfterExpand () - { - var tree = CreateTree (out Factory f, out Car car1, out _); - tree.BeginInit (); tree.EndInit (); - - tree.Bounds = new Rect (0, 0, 10, 10); - - InitFakeDriver (); - - //-+Factory - Assert.Equal (9, tree.GetContentWidth (true)); - - car1.Name = "123456789"; - - tree.Expand (f); - - //..├-123456789 - Assert.Equal (13, tree.GetContentWidth (true)); - - tree.Collapse (f); - //-+Factory - Assert.Equal (9, tree.GetContentWidth (true)); - - Application.Shutdown (); - } - - [Fact] - public void ContentWidth_VisibleVsAll () - { - var tree = CreateTree (out Factory f, out Car car1, out Car car2); - tree.BeginInit (); tree.EndInit (); - - // control only allows 1 row to be viewed at once - tree.Bounds = new Rect (0, 0, 20, 1); - - InitFakeDriver (); - - //-+Factory - Assert.Equal (9, tree.GetContentWidth (true)); - Assert.Equal (9, tree.GetContentWidth (false)); - - car1.Name = "123456789"; - car2.Name = "12345678"; - - tree.Expand (f); - - // Although expanded the bigger (longer) child node is not in the rendered area of the control - Assert.Equal (9, tree.GetContentWidth (true)); - Assert.Equal (13, tree.GetContentWidth (false)); // If you ask for the global max width it includes the longer child - - // Now that we have scrolled down 1 row we should see the big child - tree.ScrollOffsetVertical = 1; - Assert.Equal (13, tree.GetContentWidth (true)); - Assert.Equal (13, tree.GetContentWidth (false)); - - // Scroll down so only car2 is visible - tree.ScrollOffsetVertical = 2; - Assert.Equal (12, tree.GetContentWidth (true)); - Assert.Equal (13, tree.GetContentWidth (false)); - - // Scroll way down (off bottom of control even) - tree.ScrollOffsetVertical = 5; - Assert.Equal (0, tree.GetContentWidth (true)); - Assert.Equal (13, tree.GetContentWidth (false)); - - Application.Shutdown (); - } - /// - /// Tests that and behaves correctly when an object cannot be expanded (because it has no children) - /// - [Fact] - public void IsExpanded_FalseIfCannotExpand () - { - var tree = CreateTree (out Factory f, out Car c, out _); - - // expose the car by expanding the factory - tree.Expand (f); - - // car is not expanded - Assert.False (tree.IsExpanded (c)); + // should also be ignored + tree.Collapse (c); - //try to expand the car (should have no effect because cars have no children) - tree.Expand (c); - - Assert.False (tree.IsExpanded (c)); - - // should also be ignored - tree.Collapse (c); + Assert.False (tree.IsExpanded (c)); - Assert.False (tree.IsExpanded (c)); + Application.Shutdown (); + } + + /// + /// Tests illegal ranges for + /// + [Fact] + public void ScrollOffset_CannotBeNegative () + { + var tree = CreateTree (); + + Assert.Equal (0, tree.ScrollOffsetVertical); + + tree.ScrollOffsetVertical = -100; + Assert.Equal (0, tree.ScrollOffsetVertical); + + tree.ScrollOffsetVertical = 10; + Assert.Equal (10, tree.ScrollOffsetVertical); + } + + /// + /// Tests for objects that are as yet undiscovered by the tree + /// + [Fact] + public void GetScrollOffsetOf_MinusOneForUnRevealed () + { + var tree = CreateTree (out var f, out var c1, out var c2); + + // to start with the tree is collapsed and only knows about the root object + Assert.Equal (0, tree.GetScrollOffsetOf (f)); + Assert.Equal (-1, tree.GetScrollOffsetOf (c1)); + Assert.Equal (-1, tree.GetScrollOffsetOf (c2)); + + // reveal it by expanding the root object + tree.Expand (f); + + // tree now knows about children + Assert.Equal (0, tree.GetScrollOffsetOf (f)); + Assert.Equal (1, tree.GetScrollOffsetOf (c1)); + Assert.Equal (2, tree.GetScrollOffsetOf (c2)); + + // after collapsing the root node again + tree.Collapse (f); + + // tree no longer knows about the locations of these objects + Assert.Equal (0, tree.GetScrollOffsetOf (f)); + Assert.Equal (-1, tree.GetScrollOffsetOf (c1)); + Assert.Equal (-1, tree.GetScrollOffsetOf (c2)); + } + + /// + /// Simulates behind the scenes changes to an object (which children it has) and how to sync that into the tree using + /// + /// + [Fact] + public void RefreshObject_ChildRemoved () + { + var tree = CreateTree (out var f, out var c1, out var c2); + + //reveal it by expanding the root object + tree.Expand (f); + + Assert.Equal (0, tree.GetScrollOffsetOf (f)); + Assert.Equal (1, tree.GetScrollOffsetOf (c1)); + Assert.Equal (2, tree.GetScrollOffsetOf (c2)); + + // Factory now no longer makes Car c1 (only c2) + f.Cars = new [] { c2 }; + + // Tree does not know this yet + Assert.Equal (0, tree.GetScrollOffsetOf (f)); + Assert.Equal (1, tree.GetScrollOffsetOf (c1)); + Assert.Equal (2, tree.GetScrollOffsetOf (c2)); + + // If the user has selected the node c1 + tree.SelectedObject = c1; + + // When we refresh the tree + tree.RefreshObject (f); + + // Now tree knows that factory has only one child node c2 + Assert.Equal (0, tree.GetScrollOffsetOf (f)); + Assert.Equal (-1, tree.GetScrollOffsetOf (c1)); + Assert.Equal (1, tree.GetScrollOffsetOf (c2)); + + // The old selection was c1 which is now gone so selection should default to the parent of that branch (the factory) + Assert.Equal (f, tree.SelectedObject); + } + + /// + /// Tests that returns the parent object for + /// Cars (Factories). Note that the method only works once the parent branch (Factory) + /// is expanded to expose the child (Car) + /// + [Fact] + public void GetParent_ReturnsParentOnlyWhenExpanded () + { + var tree = CreateTree (out var f, out var c1, out var c2); + + Assert.Null (tree.GetParent (f)); + Assert.Null (tree.GetParent (c1)); + Assert.Null (tree.GetParent (c2)); + + // now when we expand the factory we discover the cars + tree.Expand (f); - Application.Shutdown (); - } + Assert.Null (tree.GetParent (f)); + Assert.Equal (f, tree.GetParent (c1)); + Assert.Equal (f, tree.GetParent (c2)); - /// - /// Tests illegal ranges for - /// - [Fact] - public void ScrollOffset_CannotBeNegative () - { - var tree = CreateTree (); + tree.Collapse (f); - Assert.Equal (0, tree.ScrollOffsetVertical); + Assert.Null (tree.GetParent (f)); + Assert.Null (tree.GetParent (c1)); + Assert.Null (tree.GetParent (c2)); + } - tree.ScrollOffsetVertical = -100; - Assert.Equal (0, tree.ScrollOffsetVertical); + /// + /// Tests how the tree adapts to changes in the ChildrenGetter delegate during runtime + /// when some branches are expanded and the new delegate returns children for a node that + /// previously didn't have any children + /// + [Fact] + public void RefreshObject_AfterChangingChildrenGetterDuringRuntime () + { + var tree = CreateTree (out var f, out var c1, out var c2); + + var wheel = "Shiny Wheel"; + + // Expand the Factory + tree.Expand (f); + + // c1 cannot have children + Assert.Equal (f, tree.GetParent (c1)); + + // expanding it does nothing + tree.Expand (c1); + Assert.False (tree.IsExpanded (c1)); + + // change the children getter so that now cars can have wheels + tree.TreeBuilder = new DelegateTreeBuilder (o => + // factories have cars + o is Factory ? new object [] { c1, c2 } + // cars have wheels + : new object [] { wheel }); + + // still cannot expand + tree.Expand (c1); + Assert.False (tree.IsExpanded (c1)); + + tree.RefreshObject (c1); + tree.Expand (c1); + Assert.True (tree.IsExpanded (c1)); + Assert.Equal (wheel, tree.GetChildren (c1).FirstOrDefault ()); + } - tree.ScrollOffsetVertical = 10; - Assert.Equal (10, tree.ScrollOffsetVertical); - } - - /// - /// Tests for objects that are as yet undiscovered by the tree - /// - [Fact] - public void GetScrollOffsetOf_MinusOneForUnRevealed () - { - var tree = CreateTree (out Factory f, out Car c1, out Car c2); - - // to start with the tree is collapsed and only knows about the root object - Assert.Equal (0, tree.GetScrollOffsetOf (f)); - Assert.Equal (-1, tree.GetScrollOffsetOf (c1)); - Assert.Equal (-1, tree.GetScrollOffsetOf (c2)); - - // reveal it by expanding the root object - tree.Expand (f); - - // tree now knows about children - Assert.Equal (0, tree.GetScrollOffsetOf (f)); - Assert.Equal (1, tree.GetScrollOffsetOf (c1)); - Assert.Equal (2, tree.GetScrollOffsetOf (c2)); - - // after collapsing the root node again - tree.Collapse (f); - - // tree no longer knows about the locations of these objects - Assert.Equal (0, tree.GetScrollOffsetOf (f)); - Assert.Equal (-1, tree.GetScrollOffsetOf (c1)); - Assert.Equal (-1, tree.GetScrollOffsetOf (c2)); - } - - /// - /// Simulates behind the scenes changes to an object (which children it has) and how to sync that into the tree using - /// - [Fact] - public void RefreshObject_ChildRemoved () - { - var tree = CreateTree (out Factory f, out Car c1, out Car c2); - - //reveal it by expanding the root object - tree.Expand (f); - - Assert.Equal (0, tree.GetScrollOffsetOf (f)); - Assert.Equal (1, tree.GetScrollOffsetOf (c1)); - Assert.Equal (2, tree.GetScrollOffsetOf (c2)); - - // Factory now no longer makes Car c1 (only c2) - f.Cars = new Car [] { c2 }; - - // Tree does not know this yet - Assert.Equal (0, tree.GetScrollOffsetOf (f)); - Assert.Equal (1, tree.GetScrollOffsetOf (c1)); - Assert.Equal (2, tree.GetScrollOffsetOf (c2)); - - // If the user has selected the node c1 - tree.SelectedObject = c1; - - // When we refresh the tree - tree.RefreshObject (f); - - // Now tree knows that factory has only one child node c2 - Assert.Equal (0, tree.GetScrollOffsetOf (f)); - Assert.Equal (-1, tree.GetScrollOffsetOf (c1)); - Assert.Equal (1, tree.GetScrollOffsetOf (c2)); - - // The old selection was c1 which is now gone so selection should default to the parent of that branch (the factory) - Assert.Equal (f, tree.SelectedObject); - } - - /// - /// Tests that returns the parent object for - /// Cars (Factories). Note that the method only works once the parent branch (Factory) - /// is expanded to expose the child (Car) - /// - [Fact] - public void GetParent_ReturnsParentOnlyWhenExpanded () - { - var tree = CreateTree (out Factory f, out Car c1, out Car c2); - - Assert.Null (tree.GetParent (f)); - Assert.Null (tree.GetParent (c1)); - Assert.Null (tree.GetParent (c2)); - - // now when we expand the factory we discover the cars - tree.Expand (f); - - Assert.Null (tree.GetParent (f)); - Assert.Equal (f, tree.GetParent (c1)); - Assert.Equal (f, tree.GetParent (c2)); - - tree.Collapse (f); - - Assert.Null (tree.GetParent (f)); - Assert.Null (tree.GetParent (c1)); - Assert.Null (tree.GetParent (c2)); - } - - /// - /// Tests how the tree adapts to changes in the ChildrenGetter delegate during runtime - /// when some branches are expanded and the new delegate returns children for a node that - /// previously didn't have any children - /// - [Fact] - public void RefreshObject_AfterChangingChildrenGetterDuringRuntime () - { - var tree = CreateTree (out Factory f, out Car c1, out Car c2); - - string wheel = "Shiny Wheel"; - - // Expand the Factory - tree.Expand (f); - - // c1 cannot have children - Assert.Equal (f, tree.GetParent (c1)); - - // expanding it does nothing - tree.Expand (c1); - Assert.False (tree.IsExpanded (c1)); - - // change the children getter so that now cars can have wheels - tree.TreeBuilder = new DelegateTreeBuilder ((o) => - // factories have cars - o is Factory ? new object [] { c1, c2 } - // cars have wheels - : new object [] { wheel }); - - // still cannot expand - tree.Expand (c1); - Assert.False (tree.IsExpanded (c1)); - - tree.RefreshObject (c1); - tree.Expand (c1); - Assert.True (tree.IsExpanded (c1)); - Assert.Equal (wheel, tree.GetChildren (c1).FirstOrDefault ()); - } - /// - /// Same as but - /// uses instead of - /// - [Fact] - public void RebuildTree_AfterChangingChildrenGetterDuringRuntime () - { - var tree = CreateTree (out Factory f, out Car c1, out Car c2); - - string wheel = "Shiny Wheel"; - - // Expand the Factory - tree.Expand (f); - - // c1 cannot have children - Assert.Equal (f, tree.GetParent (c1)); - - // expanding it does nothing - tree.Expand (c1); - Assert.False (tree.IsExpanded (c1)); - - // change the children getter so that now cars can have wheels - tree.TreeBuilder = new DelegateTreeBuilder ((o) => - // factories have cars - o is Factory ? new object [] { c1, c2 } - // cars have wheels - : new object [] { wheel }); - - // still cannot expand - tree.Expand (c1); - Assert.False (tree.IsExpanded (c1)); - - // Rebuild the tree - tree.RebuildTree (); - - // Rebuild should not have collapsed any branches or done anything wierd - Assert.True (tree.IsExpanded (f)); - - tree.Expand (c1); - Assert.True (tree.IsExpanded (c1)); - Assert.Equal (wheel, tree.GetChildren (c1).FirstOrDefault ()); - } - /// - /// Tests that returns the child objects for - /// the factory. Note that the method only works once the parent branch (Factory) - /// is expanded to expose the child (Car) - /// - [Fact] - public void GetChildren_ReturnsChildrenOnlyWhenExpanded () - { - var tree = CreateTree (out Factory f, out Car c1, out Car c2); - - Assert.Empty (tree.GetChildren (f)); - Assert.Empty (tree.GetChildren (c1)); - Assert.Empty (tree.GetChildren (c2)); - - // now when we expand the factory we discover the cars - tree.Expand (f); - - Assert.Contains (c1, tree.GetChildren (f)); - Assert.Contains (c2, tree.GetChildren (f)); - Assert.Empty (tree.GetChildren (c1)); - Assert.Empty (tree.GetChildren (c2)); - - tree.Collapse (f); - - Assert.Empty (tree.GetChildren (f)); - Assert.Empty (tree.GetChildren (c1)); - Assert.Empty (tree.GetChildren (c2)); - } + /// + /// Same as but + /// uses instead of + /// + [Fact] + public void RebuildTree_AfterChangingChildrenGetterDuringRuntime () + { + var tree = CreateTree (out var f, out var c1, out var c2); - [Fact] - public void TreeNode_WorksWithoutDelegate () - { - var tree = new TreeView (); + var wheel = "Shiny Wheel"; - var root = new TreeNode ("Root"); - root.Children.Add (new TreeNode ("Leaf1")); - root.Children.Add (new TreeNode ("Leaf2")); + // Expand the Factory + tree.Expand (f); - tree.AddObject (root); + // c1 cannot have children + Assert.Equal (f, tree.GetParent (c1)); - tree.Expand (root); - Assert.Equal (2, tree.GetChildren (root).Count ()); - } + // expanding it does nothing + tree.Expand (c1); + Assert.False (tree.IsExpanded (c1)); - [Fact] - public void MultiSelect_GetAllSelectedObjects () - { - var tree = new TreeView (); + // change the children getter so that now cars can have wheels + tree.TreeBuilder = new DelegateTreeBuilder (o => + // factories have cars + o is Factory ? new object [] { c1, c2 } + // cars have wheels + : new object [] { wheel }); - TreeNode l1; - TreeNode l2; - TreeNode l3; - TreeNode l4; + // still cannot expand + tree.Expand (c1); + Assert.False (tree.IsExpanded (c1)); - var root = new TreeNode ("Root"); - root.Children.Add (l1 = new TreeNode ("Leaf1")); - root.Children.Add (l2 = new TreeNode ("Leaf2")); - root.Children.Add (l3 = new TreeNode ("Leaf3")); - root.Children.Add (l4 = new TreeNode ("Leaf4")); + // Rebuild the tree + tree.RebuildTree (); - tree.AddObject (root); - tree.MultiSelect = true; + // Rebuild should not have collapsed any branches or done anything wierd + Assert.True (tree.IsExpanded (f)); - tree.Expand (root); - Assert.Empty (tree.GetAllSelectedObjects ()); + tree.Expand (c1); + Assert.True (tree.IsExpanded (c1)); + Assert.Equal (wheel, tree.GetChildren (c1).FirstOrDefault ()); + } - tree.SelectedObject = root; + /// + /// Tests that returns the child objects for + /// the factory. Note that the method only works once the parent branch (Factory) + /// is expanded to expose the child (Car) + /// + [Fact] + public void GetChildren_ReturnsChildrenOnlyWhenExpanded () + { + var tree = CreateTree (out var f, out var c1, out var c2); + + Assert.Empty (tree.GetChildren (f)); + Assert.Empty (tree.GetChildren (c1)); + Assert.Empty (tree.GetChildren (c2)); + + // now when we expand the factory we discover the cars + tree.Expand (f); + + Assert.Contains (c1, tree.GetChildren (f)); + Assert.Contains (c2, tree.GetChildren (f)); + Assert.Empty (tree.GetChildren (c1)); + Assert.Empty (tree.GetChildren (c2)); + + tree.Collapse (f); + + Assert.Empty (tree.GetChildren (f)); + Assert.Empty (tree.GetChildren (c1)); + Assert.Empty (tree.GetChildren (c2)); + } - Assert.Single (tree.GetAllSelectedObjects (), root); + [Fact] + public void TreeNode_WorksWithoutDelegate () + { + var tree = new TreeView (); - // move selection down 1 - tree.AdjustSelection (1, false); + var root = new TreeNode ("Root"); + root.Children.Add (new TreeNode ("Leaf1")); + root.Children.Add (new TreeNode ("Leaf2")); - Assert.Single (tree.GetAllSelectedObjects (), l1); + tree.AddObject (root); - // expand selection down 2 (e.g. shift down twice) - tree.AdjustSelection (1, true); - tree.AdjustSelection (1, true); + tree.Expand (root); + Assert.Equal (2, tree.GetChildren (root).Count ()); + } - Assert.Equal (3, tree.GetAllSelectedObjects ().Count ()); - Assert.Contains (l1, tree.GetAllSelectedObjects ()); - Assert.Contains (l2, tree.GetAllSelectedObjects ()); - Assert.Contains (l3, tree.GetAllSelectedObjects ()); + [Fact] + public void MultiSelect_GetAllSelectedObjects () + { + var tree = new TreeView (); - tree.Collapse (root); + TreeNode l1; + TreeNode l2; + TreeNode l3; + TreeNode l4; - // No selected objects since the root was collapsed - Assert.Empty (tree.GetAllSelectedObjects ()); - } + var root = new TreeNode ("Root"); + root.Children.Add (l1 = new TreeNode ("Leaf1")); + root.Children.Add (l2 = new TreeNode ("Leaf2")); + root.Children.Add (l3 = new TreeNode ("Leaf3")); + root.Children.Add (l4 = new TreeNode ("Leaf4")); - [Fact] - public void ObjectActivated_Called () - { - var tree = CreateTree (out Factory f, out Car car1, out _); + tree.AddObject (root); + tree.MultiSelect = true; - InitFakeDriver (); + tree.Expand (root); + Assert.Empty (tree.GetAllSelectedObjects ()); - object activated = null; - bool called = false; + tree.SelectedObject = root; - // register for the event - tree.ObjectActivated += (s, e) => { - activated = e.ActivatedObject; - called = true; - }; + Assert.Single (tree.GetAllSelectedObjects (), root); - Assert.False (called); + // move selection down 1 + tree.AdjustSelection (1); - // no object is selected yet so no event should happen - tree.NewKeyDownEvent (new (KeyCode.Enter)); + Assert.Single (tree.GetAllSelectedObjects (), l1); - Assert.Null (activated); - Assert.False (called); + // expand selection down 2 (e.g. shift down twice) + tree.AdjustSelection (1, true); + tree.AdjustSelection (1, true); - // down to select factory - tree.NewKeyDownEvent (new (KeyCode.CursorDown)); + Assert.Equal (3, tree.GetAllSelectedObjects ().Count ()); + Assert.Contains (l1, tree.GetAllSelectedObjects ()); + Assert.Contains (l2, tree.GetAllSelectedObjects ()); + Assert.Contains (l3, tree.GetAllSelectedObjects ()); - tree.NewKeyDownEvent (new (KeyCode.Enter)); + tree.Collapse (root); - Assert.True (called); - Assert.Same (f, activated); + // No selected objects since the root was collapsed + Assert.Empty (tree.GetAllSelectedObjects ()); + } - Application.Shutdown (); - } + [Fact] + public void ObjectActivated_Called () + { + var tree = CreateTree (out var f, out var car1, out _); - [Fact] - public void GoTo_OnlyAppliesToExposedObjects () - { - var tree = CreateTree (out Factory f, out Car car1, out _); - tree.BeginInit (); tree.EndInit (); + InitFakeDriver (); - // Make tree bounds 1 in height so that EnsureVisible always requires updating scroll offset - tree.Bounds = new Rect (0, 0, 50, 1); + object activated = null; + var called = false; - Assert.Null (tree.SelectedObject); - Assert.Equal (0, tree.ScrollOffsetVertical); + // register for the event + tree.ObjectActivated += (s, e) => { + activated = e.ActivatedObject; + called = true; + }; - // car 1 is not yet exposed - tree.GoTo (car1); + Assert.False (called); - Assert.Null (tree.SelectedObject); - Assert.Equal (0, tree.ScrollOffsetVertical); + // no object is selected yet so no event should happen + tree.NewKeyDownEvent (new Key (KeyCode.Enter)); - tree.Expand (f); + Assert.Null (activated); + Assert.False (called); - // Car1 is now exposed by expanding the factory - tree.GoTo (car1); + // down to select factory + tree.NewKeyDownEvent (new Key (KeyCode.CursorDown)); - Assert.Equal (car1, tree.SelectedObject); - Assert.Equal (1, tree.ScrollOffsetVertical); - } + tree.NewKeyDownEvent (new Key (KeyCode.Enter)); - [Fact] - public void GoToEnd_ShouldNotFailOnEmptyTreeView () - { - var tree = new TreeView (); + Assert.True (called); + Assert.Same (f, activated); - var exception = Record.Exception (() => tree.GoToEnd ()); + Application.Shutdown (); + } - Assert.Null (exception); - } + [Fact] + public void GoTo_OnlyAppliesToExposedObjects () + { + var tree = CreateTree (out var f, out var car1, out _); + tree.BeginInit (); + tree.EndInit (); - [Fact] - public void ObjectActivated_CustomKey () - { - var tree = CreateTree (out Factory f, out Car car1, out _); + // Make tree bounds 1 in height so that EnsureVisible always requires updating scroll offset + tree.Bounds = new Rect (0, 0, 50, 1); - InitFakeDriver (); + Assert.Null (tree.SelectedObject); + Assert.Equal (0, tree.ScrollOffsetVertical); - tree.ObjectActivationKey = KeyCode.Delete; - object activated = null; - bool called = false; + // car 1 is not yet exposed + tree.GoTo (car1); - // register for the event - tree.ObjectActivated += (s, e) => { - activated = e.ActivatedObject; - called = true; - }; + Assert.Null (tree.SelectedObject); + Assert.Equal (0, tree.ScrollOffsetVertical); - Assert.False (called); + tree.Expand (f); - // no object is selected yet so no event should happen - tree.NewKeyDownEvent (new (KeyCode.Enter)); + // Car1 is now exposed by expanding the factory + tree.GoTo (car1); - Assert.Null (activated); - Assert.False (called); + Assert.Equal (car1, tree.SelectedObject); + Assert.Equal (1, tree.ScrollOffsetVertical); + } - // down to select factory - tree.NewKeyDownEvent (new (KeyCode.CursorDown)); + [Fact] + public void GoToEnd_ShouldNotFailOnEmptyTreeView () + { + var tree = new TreeView (); - tree.NewKeyDownEvent (new (KeyCode.Enter)); + var exception = Record.Exception (() => tree.GoToEnd ()); - // Enter is not the activation key in this unit test - Assert.Null (activated); - Assert.False (called); + Assert.Null (exception); + } - // Delete is the activation key in this test so should result in activation occurring - tree.NewKeyDownEvent (new (KeyCode.Delete)); + [Fact] + public void ObjectActivated_CustomKey () + { + var tree = CreateTree (out var f, out var car1, out _); - Assert.True (called); - Assert.Same (f, activated); + InitFakeDriver (); - Application.Shutdown (); - } + tree.ObjectActivationKey = KeyCode.Delete; + object activated = null; + var called = false; - [Fact] - public void ObjectActivationButton_DoubleClick () - { - var tree = CreateTree (out Factory f, out Car car1, out _); + // register for the event + tree.ObjectActivated += (s, e) => { + activated = e.ActivatedObject; + called = true; + }; - InitFakeDriver (); + Assert.False (called); - object activated = null; - bool called = false; + // no object is selected yet so no event should happen + tree.NewKeyDownEvent (new Key (KeyCode.Enter)); - // register for the event - tree.ObjectActivated += (s, e) => { - activated = e.ActivatedObject; - called = true; - }; + Assert.Null (activated); + Assert.False (called); - Assert.False (called); + // down to select factory + tree.NewKeyDownEvent (new Key (KeyCode.CursorDown)); - // double click triggers activation - tree.MouseEvent (new MouseEvent () { Y = 0, Flags = MouseFlags.Button1DoubleClicked }); + tree.NewKeyDownEvent (new Key (KeyCode.Enter)); - Assert.True (called); - Assert.Same (f, activated); - Assert.Same (f, tree.SelectedObject); + // Enter is not the activation key in this unit test + Assert.Null (activated); + Assert.False (called); - Application.Shutdown (); - } + // Delete is the activation key in this test so should result in activation occurring + tree.NewKeyDownEvent (new Key (KeyCode.Delete)); - [Fact] - public void ObjectActivationButton_SetToNull () - { - var tree = CreateTree (out Factory f, out Car car1, out _); + Assert.True (called); + Assert.Same (f, activated); - InitFakeDriver (); + Application.Shutdown (); + } + + [Fact] + public void ObjectActivationButton_DoubleClick () + { + var tree = CreateTree (out var f, out var car1, out _); + + InitFakeDriver (); - // disable activation - tree.ObjectActivationButton = null; + object activated = null; + var called = false; + + // register for the event + tree.ObjectActivated += (s, e) => { + activated = e.ActivatedObject; + called = true; + }; - object activated = null; - bool called = false; + Assert.False (called); - // register for the event - tree.ObjectActivated += (s, e) => { - activated = e.ActivatedObject; - called = true; - }; + // double click triggers activation + tree.MouseEvent (new MouseEvent { Y = 0, Flags = MouseFlags.Button1DoubleClicked }); - Assert.False (called); + Assert.True (called); + Assert.Same (f, activated); + Assert.Same (f, tree.SelectedObject); - // double click does nothing because we changed button to null - tree.MouseEvent (new MouseEvent () { Y = 0, Flags = MouseFlags.Button1DoubleClicked }); + Application.Shutdown (); + } - Assert.False (called); - Assert.Null (activated); - Assert.Null (tree.SelectedObject); + [Fact] + public void ObjectActivationButton_SetToNull () + { + var tree = CreateTree (out var f, out var car1, out _); - Application.Shutdown (); - } + InitFakeDriver (); - [Fact] - public void ObjectActivationButton_RightClick () - { - var tree = CreateTree (out Factory f, out Car car1, out _); + // disable activation + tree.ObjectActivationButton = null; - InitFakeDriver (); + object activated = null; + var called = false; - tree.ObjectActivationButton = MouseFlags.Button2Clicked; - tree.ExpandAll (); + // register for the event + tree.ObjectActivated += (s, e) => { + activated = e.ActivatedObject; + called = true; + }; - object activated = null; - bool called = false; + Assert.False (called); - // register for the event - tree.ObjectActivated += (s, e) => { - activated = e.ActivatedObject; - called = true; - }; + // double click does nothing because we changed button to null + tree.MouseEvent (new MouseEvent { Y = 0, Flags = MouseFlags.Button1DoubleClicked }); - Assert.False (called); + Assert.False (called); + Assert.Null (activated); + Assert.Null (tree.SelectedObject); - // double click does nothing because we changed button binding to right click - tree.MouseEvent (new MouseEvent () { Y = 1, Flags = MouseFlags.Button1DoubleClicked }); + Application.Shutdown (); + } - Assert.Null (activated); - Assert.False (called); + [Fact] + public void ObjectActivationButton_RightClick () + { + var tree = CreateTree (out var f, out var car1, out _); - tree.MouseEvent (new MouseEvent () { Y = 1, Flags = MouseFlags.Button2Clicked }); + InitFakeDriver (); - Assert.True (called); - Assert.Same (car1, activated); - Assert.Same (car1, tree.SelectedObject); + tree.ObjectActivationButton = MouseFlags.Button2Clicked; + tree.ExpandAll (); - Application.Shutdown (); - } + object activated = null; + var called = false; + // register for the event + tree.ObjectActivated += (s, e) => { + activated = e.ActivatedObject; + called = true; + }; - /// - /// Simulates behind the scenes changes to an object (which children it has) and how to sync that into the tree using - /// - [Fact] - public void RefreshObject_EqualityTest () - { - var obj1 = new EqualityTestObject () { Name = "Bob", Age = 1 }; - var obj2 = new EqualityTestObject () { Name = "Bob", Age = 2 }; ; + Assert.False (called); - string root = "root"; + // double click does nothing because we changed button binding to right click + tree.MouseEvent (new MouseEvent { Y = 1, Flags = MouseFlags.Button1DoubleClicked }); - var tree = new TreeView (); - tree.TreeBuilder = new DelegateTreeBuilder ((s) => ReferenceEquals (s, root) ? new object [] { obj1 } : null); - tree.AddObject (root); + Assert.Null (activated); + Assert.False (called); - // Tree is not expanded so the root has no children yet - Assert.Empty (tree.GetChildren (root)); + tree.MouseEvent (new MouseEvent { Y = 1, Flags = MouseFlags.Button2Clicked }); - tree.Expand (root); + Assert.True (called); + Assert.Same (car1, activated); + Assert.Same (car1, tree.SelectedObject); - // now that the tree is expanded we should get our child returned - Assert.Equal (1, tree.GetChildren (root).Count (child => ReferenceEquals (obj1, child))); + Application.Shutdown (); + } - // change the getter to return an Equal object (but not the same reference - obj2) - tree.TreeBuilder = new DelegateTreeBuilder ((s) => ReferenceEquals (s, root) ? new object [] { obj2 } : null); - // tree has cached the knowledge of what children the root has so won't know about the change (we still get obj1) - Assert.Equal (1, tree.GetChildren (root).Count (child => ReferenceEquals (obj1, child))); + /// + /// Simulates behind the scenes changes to an object (which children it has) and how to sync that into the tree using + /// + /// + [Fact] + public void RefreshObject_EqualityTest () + { + var obj1 = new EqualityTestObject { Name = "Bob", Age = 1 }; + var obj2 = new EqualityTestObject { Name = "Bob", Age = 2 }; + ; - // now that we refresh the root we should get the new child reference (obj2) - tree.RefreshObject (root); - Assert.Equal (1, tree.GetChildren (root).Count (child => ReferenceEquals (obj2, child))); + var root = "root"; - } - [Fact, AutoInitShutdown] - public void TestGetObjectOnRow () - { - var tv = new TreeView { Width = 20, Height = 10 }; - tv.BeginInit (); tv.EndInit (); - var n1 = new TreeNode ("normal"); - var n1_1 = new TreeNode ("pink"); - var n1_2 = new TreeNode ("normal"); - n1.Children.Add (n1_1); - n1.Children.Add (n1_2); + var tree = new TreeView (); + tree.TreeBuilder = new DelegateTreeBuilder (s => ReferenceEquals (s, root) ? new object [] { obj1 } : null); + tree.AddObject (root); - var n2 = new TreeNode ("pink"); - tv.AddObject (n1); - tv.AddObject (n2); - tv.Expand (n1); + // Tree is not expanded so the root has no children yet + Assert.Empty (tree.GetChildren (root)); - tv.ColorScheme = new ColorScheme (); - tv.LayoutSubviews (); - tv.Draw (); + tree.Expand (root); - TestHelpers.AssertDriverContentsAre ( -@"├-normal + // now that the tree is expanded we should get our child returned + Assert.Equal (1, tree.GetChildren (root).Count (child => ReferenceEquals (obj1, child))); + + // change the getter to return an Equal object (but not the same reference - obj2) + tree.TreeBuilder = new DelegateTreeBuilder (s => ReferenceEquals (s, root) ? new object [] { obj2 } : null); + + // tree has cached the knowledge of what children the root has so won't know about the change (we still get obj1) + Assert.Equal (1, tree.GetChildren (root).Count (child => ReferenceEquals (obj1, child))); + + // now that we refresh the root we should get the new child reference (obj2) + tree.RefreshObject (root); + Assert.Equal (1, tree.GetChildren (root).Count (child => ReferenceEquals (obj2, child))); + + } + + [Fact, AutoInitShutdown] + public void TestGetObjectOnRow () + { + var tv = new TreeView { Width = 20, Height = 10 }; + tv.BeginInit (); + tv.EndInit (); + var n1 = new TreeNode ("normal"); + var n1_1 = new TreeNode ("pink"); + var n1_2 = new TreeNode ("normal"); + n1.Children.Add (n1_1); + n1.Children.Add (n1_2); + + var n2 = new TreeNode ("pink"); + tv.AddObject (n1); + tv.AddObject (n2); + tv.Expand (n1); + + tv.ColorScheme = new ColorScheme (); + tv.LayoutSubviews (); + tv.Draw (); + + TestHelpers.AssertDriverContentsAre ( + @"├-normal │ ├─pink │ └─normal └─pink -", output); +", _output); - Assert.Same (n1, tv.GetObjectOnRow (0)); - Assert.Same (n1_1, tv.GetObjectOnRow (1)); - Assert.Same (n1_2, tv.GetObjectOnRow (2)); - Assert.Same (n2, tv.GetObjectOnRow (3)); - Assert.Null (tv.GetObjectOnRow (4)); + Assert.Same (n1, tv.GetObjectOnRow (0)); + Assert.Same (n1_1, tv.GetObjectOnRow (1)); + Assert.Same (n1_2, tv.GetObjectOnRow (2)); + Assert.Same (n2, tv.GetObjectOnRow (3)); + Assert.Null (tv.GetObjectOnRow (4)); - tv.Collapse (n1); + tv.Collapse (n1); - tv.Draw (); + tv.Draw (); - TestHelpers.AssertDriverContentsAre ( -@"├+normal + TestHelpers.AssertDriverContentsAre ( + @"├+normal └─pink -", output); - - Assert.Same (n1, tv.GetObjectOnRow (0)); - Assert.Same (n2, tv.GetObjectOnRow (1)); - Assert.Null (tv.GetObjectOnRow (2)); - Assert.Null (tv.GetObjectOnRow (3)); - Assert.Null (tv.GetObjectOnRow (4)); - } - - [Fact, AutoInitShutdown] - public void TestGetObjectRow () - { - var tv = new TreeView { Width = 20, Height = 10 }; - - var n1 = new TreeNode ("normal"); - var n1_1 = new TreeNode ("pink"); - var n1_2 = new TreeNode ("normal"); - n1.Children.Add (n1_1); - n1.Children.Add (n1_2); - - var n2 = new TreeNode ("pink"); - tv.AddObject (n1); - tv.AddObject (n2); - tv.Expand (n1); - - tv.ColorScheme = new ColorScheme (); - tv.LayoutSubviews (); - tv.Draw (); - - TestHelpers.AssertDriverContentsAre ( -@"├-normal +", _output); + + Assert.Same (n1, tv.GetObjectOnRow (0)); + Assert.Same (n2, tv.GetObjectOnRow (1)); + Assert.Null (tv.GetObjectOnRow (2)); + Assert.Null (tv.GetObjectOnRow (3)); + Assert.Null (tv.GetObjectOnRow (4)); + } + + [Fact, AutoInitShutdown] + public void TestGetObjectRow () + { + var tv = new TreeView { Width = 20, Height = 10 }; + + var n1 = new TreeNode ("normal"); + var n1_1 = new TreeNode ("pink"); + var n1_2 = new TreeNode ("normal"); + n1.Children.Add (n1_1); + n1.Children.Add (n1_2); + + var n2 = new TreeNode ("pink"); + tv.AddObject (n1); + tv.AddObject (n2); + tv.Expand (n1); + + tv.ColorScheme = new ColorScheme (); + tv.LayoutSubviews (); + tv.Draw (); + + TestHelpers.AssertDriverContentsAre ( + @"├-normal │ ├─pink │ └─normal └─pink -", output); +", _output); - Assert.Equal (0, tv.GetObjectRow (n1)); - Assert.Equal (1, tv.GetObjectRow (n1_1)); - Assert.Equal (2, tv.GetObjectRow (n1_2)); - Assert.Equal (3, tv.GetObjectRow (n2)); + Assert.Equal (0, tv.GetObjectRow (n1)); + Assert.Equal (1, tv.GetObjectRow (n1_1)); + Assert.Equal (2, tv.GetObjectRow (n1_2)); + Assert.Equal (3, tv.GetObjectRow (n2)); - tv.Collapse (n1); + tv.Collapse (n1); - tv.LayoutSubviews (); - tv.Draw (); + tv.LayoutSubviews (); + tv.Draw (); - TestHelpers.AssertDriverContentsAre ( -@"├+normal + TestHelpers.AssertDriverContentsAre ( + @"├+normal └─pink -", output); - Assert.Equal (0, tv.GetObjectRow (n1)); - Assert.Null (tv.GetObjectRow (n1_1)); - Assert.Null (tv.GetObjectRow (n1_2)); - Assert.Equal (1, tv.GetObjectRow (n2)); - - // scroll down 1 - tv.ScrollOffsetVertical = 1; - - tv.LayoutSubviews (); - tv.Draw (); - - TestHelpers.AssertDriverContentsAre ( -@"└─pink -", output); - Assert.Equal (-1, tv.GetObjectRow (n1)); - Assert.Null (tv.GetObjectRow (n1_1)); - Assert.Null (tv.GetObjectRow (n1_2)); - Assert.Equal (0, tv.GetObjectRow (n2)); - } - [Fact, AutoInitShutdown] - public void TestTreeViewColor () - { - var tv = new TreeView { Width = 20, Height = 10 }; - tv.BeginInit (); - tv.EndInit (); - var n1 = new TreeNode ("normal"); - var n1_1 = new TreeNode ("pink"); - var n1_2 = new TreeNode ("normal"); - n1.Children.Add (n1_1); - n1.Children.Add (n1_2); - - var n2 = new TreeNode ("pink"); - tv.AddObject (n1); - tv.AddObject (n2); - tv.Expand (n1); - - tv.ColorScheme = new ColorScheme (); - tv.LayoutSubviews (); - tv.Draw (); - - // create a new color scheme - var pink = new Attribute (Color.Magenta, Color.Black); - var hotpink = new Attribute (Color.BrightMagenta, Color.Black); - - // Normal drawing of the tree view - TestHelpers.AssertDriverContentsAre (@" +", _output); + Assert.Equal (0, tv.GetObjectRow (n1)); + Assert.Null (tv.GetObjectRow (n1_1)); + Assert.Null (tv.GetObjectRow (n1_2)); + Assert.Equal (1, tv.GetObjectRow (n2)); + + // scroll down 1 + tv.ScrollOffsetVertical = 1; + + tv.LayoutSubviews (); + tv.Draw (); + + TestHelpers.AssertDriverContentsAre ( + @"└─pink +", _output); + Assert.Equal (-1, tv.GetObjectRow (n1)); + Assert.Null (tv.GetObjectRow (n1_1)); + Assert.Null (tv.GetObjectRow (n1_2)); + Assert.Equal (0, tv.GetObjectRow (n2)); + } + + [Fact, AutoInitShutdown] + public void TestTreeViewColor () + { + var tv = new TreeView { Width = 20, Height = 10 }; + tv.BeginInit (); + tv.EndInit (); + var n1 = new TreeNode ("normal"); + var n1_1 = new TreeNode ("pink"); + var n1_2 = new TreeNode ("normal"); + n1.Children.Add (n1_1); + n1.Children.Add (n1_2); + + var n2 = new TreeNode ("pink"); + tv.AddObject (n1); + tv.AddObject (n2); + tv.Expand (n1); + + tv.ColorScheme = new ColorScheme (); + tv.LayoutSubviews (); + tv.Draw (); + + // create a new color scheme + var pink = new Attribute (Color.Magenta, Color.Black); + var hotpink = new Attribute (Color.BrightMagenta, Color.Black); + + // Normal drawing of the tree view + TestHelpers.AssertDriverContentsAre (@" ├-normal │ ├─pink │ └─normal └─pink -", output); - // Should all be the same color - TestHelpers.AssertDriverColorsAre (@" +", _output); + // Should all be the same color + TestHelpers.AssertDriverColorsAre (@" 0000000000 0000000000 0000000000 0000000000 -", driver: Application.Driver, - new [] { tv.ColorScheme.Normal, pink }); +", Application.Driver, tv.ColorScheme.Normal, pink); - var pinkScheme = new ColorScheme { - Normal = pink, - Focus = hotpink - }; + var pinkScheme = new ColorScheme { + Normal = pink, + Focus = hotpink + }; - // and a delegate that uses the pink color scheme - // for nodes "pink" - tv.ColorGetter = (n) => n.Text.Equals ("pink") ? pinkScheme : null; + // and a delegate that uses the pink color scheme + // for nodes "pink" + tv.ColorGetter = n => n.Text.Equals ("pink") ? pinkScheme : null; - // redraw now that the custom color - // delegate is registered - tv.Draw (); + // redraw now that the custom color + // delegate is registered + tv.Draw (); - // Same text - TestHelpers.AssertDriverContentsAre (@" + // Same text + TestHelpers.AssertDriverContentsAre (@" ├-normal │ ├─pink │ └─normal └─pink -", output); - // but now the item (only not lines) appear - // in pink when they are the word "pink" - TestHelpers.AssertDriverColorsAre (@" +", _output); + // but now the item (only not lines) appear + // in pink when they are the word "pink" + TestHelpers.AssertDriverColorsAre (@" 00000000 00001111 0000000000 001111 -", driver: Application.Driver, - new [] { tv.ColorScheme.Normal, pink }); - } +", Application.Driver, tv.ColorScheme.Normal, pink); + } - [Fact, AutoInitShutdown] - public void TestBottomlessTreeView_MaxDepth_5 () - { - var tv = new TreeView () { Width = 20, Height = 10 }; + [Fact, AutoInitShutdown] + public void TestBottomlessTreeView_MaxDepth_5 () + { + var tv = new TreeView { Width = 20, Height = 10 }; - tv.TreeBuilder = new DelegateTreeBuilder ( - (s) => new [] { (int.Parse (s) + 1).ToString () } - ); + tv.TreeBuilder = new DelegateTreeBuilder ( + s => new [] { (int.Parse (s) + 1).ToString () } + ); - tv.AddObject ("1"); - tv.ColorScheme = new ColorScheme (); + tv.AddObject ("1"); + tv.ColorScheme = new ColorScheme (); - tv.LayoutSubviews (); - tv.Draw (); + tv.LayoutSubviews (); + tv.Draw (); - // Nothing expanded - TestHelpers.AssertDriverContentsAre ( -@"└+1 -", output); - tv.MaxDepth = 5; - tv.ExpandAll (); + // Nothing expanded + TestHelpers.AssertDriverContentsAre ( + @"└+1 +", _output); + tv.MaxDepth = 5; + tv.ExpandAll (); - tv.Draw (); + tv.Draw (); - // Normal drawing of the tree view - TestHelpers.AssertDriverContentsAre ( -@" + // Normal drawing of the tree view + TestHelpers.AssertDriverContentsAre ( + @" └-1 └-2 └-3 └-4 └-5 └─6 -", output); - Assert.False (tv.CanExpand ("6")); - Assert.False (tv.IsExpanded ("6")); +", _output); + Assert.False (tv.CanExpand ("6")); + Assert.False (tv.IsExpanded ("6")); - tv.Collapse ("6"); + tv.Collapse ("6"); - Assert.False (tv.CanExpand ("6")); - Assert.False (tv.IsExpanded ("6")); + Assert.False (tv.CanExpand ("6")); + Assert.False (tv.IsExpanded ("6")); - tv.Collapse ("5"); + tv.Collapse ("5"); - Assert.True (tv.CanExpand ("5")); - Assert.False (tv.IsExpanded ("5")); + Assert.True (tv.CanExpand ("5")); + Assert.False (tv.IsExpanded ("5")); - tv.Draw (); + tv.Draw (); - // Normal drawing of the tree view - TestHelpers.AssertDriverContentsAre ( -@" + // Normal drawing of the tree view + TestHelpers.AssertDriverContentsAre ( + @" └-1 └-2 └-3 └-4 └+5 -", output); - } - - [Fact, AutoInitShutdown] - public void TestBottomlessTreeView_MaxDepth_3 () - { - var tv = new TreeView () { Width = 20, Height = 10 }; - - tv.TreeBuilder = new DelegateTreeBuilder ( - (s) => new [] { (int.Parse (s) + 1).ToString () } - ); - - tv.AddObject ("1"); - tv.ColorScheme = new ColorScheme (); - - tv.LayoutSubviews (); - tv.Draw (); - - // Nothing expanded - TestHelpers.AssertDriverContentsAre ( -@"└+1 -", output); - tv.MaxDepth = 3; - tv.ExpandAll (); - tv.Draw (); - - // Normal drawing of the tree view - TestHelpers.AssertDriverContentsAre ( -@" +", _output); + } + + [Fact, AutoInitShutdown] + public void TestBottomlessTreeView_MaxDepth_3 () + { + var tv = new TreeView { Width = 20, Height = 10 }; + + tv.TreeBuilder = new DelegateTreeBuilder ( + s => new [] { (int.Parse (s) + 1).ToString () } + ); + + tv.AddObject ("1"); + tv.ColorScheme = new ColorScheme (); + + tv.LayoutSubviews (); + tv.Draw (); + + // Nothing expanded + TestHelpers.AssertDriverContentsAre ( + @"└+1 +", _output); + tv.MaxDepth = 3; + tv.ExpandAll (); + tv.Draw (); + + // Normal drawing of the tree view + TestHelpers.AssertDriverContentsAre ( + @" └-1 └-2 └-3 └─4 -", output); - } - [Fact, AutoInitShutdown] - public void TestTreeView_DrawLineEvent () - { - var tv = new TreeView { Width = 20, Height = 10 }; - - var eventArgs = new List> (); - - tv.DrawLine += (s, e) => { - eventArgs.Add (e); - }; - - var n1 = new TreeNode ("root one"); - var n1_1 = new TreeNode ("leaf 1"); - var n1_2 = new TreeNode ("leaf 2"); - n1.Children.Add (n1_1); - n1.Children.Add (n1_2); - - var n2 = new TreeNode ("root two"); - tv.AddObject (n1); - tv.AddObject (n2); - tv.Expand (n1); - - tv.ColorScheme = new ColorScheme (); - tv.LayoutSubviews (); - tv.Draw (); - - // Normal drawing of the tree view - TestHelpers.AssertDriverContentsAre ( -@" +", _output); + } + + [Fact, AutoInitShutdown] + public void TestTreeView_DrawLineEvent () + { + var tv = new TreeView { Width = 20, Height = 10 }; + + var eventArgs = new List> (); + + tv.DrawLine += (s, e) => { + eventArgs.Add (e); + }; + + var n1 = new TreeNode ("root one"); + var n1_1 = new TreeNode ("leaf 1"); + var n1_2 = new TreeNode ("leaf 2"); + n1.Children.Add (n1_1); + n1.Children.Add (n1_2); + + var n2 = new TreeNode ("root two"); + tv.AddObject (n1); + tv.AddObject (n2); + tv.Expand (n1); + + tv.ColorScheme = new ColorScheme (); + tv.LayoutSubviews (); + tv.Draw (); + + // Normal drawing of the tree view + TestHelpers.AssertDriverContentsAre ( + @" ├-root one │ ├─leaf 1 │ └─leaf 2 └─root two -", output); - Assert.Equal (4, eventArgs.Count ()); - - Assert.Equal (0, eventArgs [0].Y); - Assert.Equal (1, eventArgs [1].Y); - Assert.Equal (2, eventArgs [2].Y); - Assert.Equal (3, eventArgs [3].Y); - - Assert.All (eventArgs, ea => Assert.Equal (ea.Tree, tv)); - Assert.All (eventArgs, ea => Assert.False (ea.Handled)); - - Assert.Equal ("├-root one", eventArgs [0].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ()); - Assert.Equal ("│ ├─leaf 1", eventArgs [1].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ()); - Assert.Equal ("│ └─leaf 2", eventArgs [2].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ()); - Assert.Equal ("└─root two", eventArgs [3].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ()); - - Assert.Equal (1, eventArgs [0].IndexOfExpandCollapseSymbol); - Assert.Equal (3, eventArgs [1].IndexOfExpandCollapseSymbol); - Assert.Equal (3, eventArgs [2].IndexOfExpandCollapseSymbol); - Assert.Equal (1, eventArgs [3].IndexOfExpandCollapseSymbol); - - Assert.Equal (2, eventArgs [0].IndexOfModelText); - Assert.Equal (4, eventArgs [1].IndexOfModelText); - Assert.Equal (4, eventArgs [2].IndexOfModelText); - Assert.Equal (2, eventArgs [3].IndexOfModelText); - - - Assert.Equal ("root one", eventArgs [0].Model.Text); - Assert.Equal ("leaf 1", eventArgs [1].Model.Text); - Assert.Equal ("leaf 2", eventArgs [2].Model.Text); - Assert.Equal ("root two", eventArgs [3].Model.Text); - } - - [Fact, AutoInitShutdown] - public void TestTreeView_DrawLineEvent_WithScrolling () - { - var tv = new TreeView { Width = 20, Height = 10 }; - - var eventArgs = new List> (); - - tv.DrawLine += (s, e) => { - eventArgs.Add (e); - }; - - tv.ScrollOffsetHorizontal = 3; - tv.ScrollOffsetVertical = 1; - - var n1 = new TreeNode ("root one"); - var n1_1 = new TreeNode ("leaf 1"); - var n1_2 = new TreeNode ("leaf 2"); - n1.Children.Add (n1_1); - n1.Children.Add (n1_2); - - var n2 = new TreeNode ("root two"); - tv.AddObject (n1); - tv.AddObject (n2); - tv.Expand (n1); - - tv.ColorScheme = new ColorScheme (); - tv.LayoutSubviews (); - tv.Draw (); - - // Normal drawing of the tree view - TestHelpers.AssertDriverContentsAre ( -@" +", _output); + Assert.Equal (4, eventArgs.Count ()); + + Assert.Equal (0, eventArgs [0].Y); + Assert.Equal (1, eventArgs [1].Y); + Assert.Equal (2, eventArgs [2].Y); + Assert.Equal (3, eventArgs [3].Y); + + Assert.All (eventArgs, ea => Assert.Equal (ea.Tree, tv)); + Assert.All (eventArgs, ea => Assert.False (ea.Handled)); + + Assert.Equal ("├-root one", eventArgs [0].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ()); + Assert.Equal ("│ ├─leaf 1", eventArgs [1].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ()); + Assert.Equal ("│ └─leaf 2", eventArgs [2].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ()); + Assert.Equal ("└─root two", eventArgs [3].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ()); + + Assert.Equal (1, eventArgs [0].IndexOfExpandCollapseSymbol); + Assert.Equal (3, eventArgs [1].IndexOfExpandCollapseSymbol); + Assert.Equal (3, eventArgs [2].IndexOfExpandCollapseSymbol); + Assert.Equal (1, eventArgs [3].IndexOfExpandCollapseSymbol); + + Assert.Equal (2, eventArgs [0].IndexOfModelText); + Assert.Equal (4, eventArgs [1].IndexOfModelText); + Assert.Equal (4, eventArgs [2].IndexOfModelText); + Assert.Equal (2, eventArgs [3].IndexOfModelText); + + + Assert.Equal ("root one", eventArgs [0].Model.Text); + Assert.Equal ("leaf 1", eventArgs [1].Model.Text); + Assert.Equal ("leaf 2", eventArgs [2].Model.Text); + Assert.Equal ("root two", eventArgs [3].Model.Text); + } + + [Fact, AutoInitShutdown] + public void TestTreeView_DrawLineEvent_WithScrolling () + { + var tv = new TreeView { Width = 20, Height = 10 }; + + var eventArgs = new List> (); + + tv.DrawLine += (s, e) => { + eventArgs.Add (e); + }; + + tv.ScrollOffsetHorizontal = 3; + tv.ScrollOffsetVertical = 1; + + var n1 = new TreeNode ("root one"); + var n1_1 = new TreeNode ("leaf 1"); + var n1_2 = new TreeNode ("leaf 2"); + n1.Children.Add (n1_1); + n1.Children.Add (n1_2); + + var n2 = new TreeNode ("root two"); + tv.AddObject (n1); + tv.AddObject (n2); + tv.Expand (n1); + + tv.ColorScheme = new ColorScheme (); + tv.LayoutSubviews (); + tv.Draw (); + + // Normal drawing of the tree view + TestHelpers.AssertDriverContentsAre ( + @" ─leaf 1 ─leaf 2 oot two -", output); - Assert.Equal (3, eventArgs.Count ()); +", _output); + Assert.Equal (3, eventArgs.Count ()); - Assert.Equal (0, eventArgs [0].Y); - Assert.Equal (1, eventArgs [1].Y); - Assert.Equal (2, eventArgs [2].Y); + Assert.Equal (0, eventArgs [0].Y); + Assert.Equal (1, eventArgs [1].Y); + Assert.Equal (2, eventArgs [2].Y); - Assert.All (eventArgs, ea => Assert.Equal (ea.Tree, tv)); - Assert.All (eventArgs, ea => Assert.False (ea.Handled)); + Assert.All (eventArgs, ea => Assert.Equal (ea.Tree, tv)); + Assert.All (eventArgs, ea => Assert.False (ea.Handled)); - Assert.Equal ("─leaf 1", eventArgs [0].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ()); - Assert.Equal ("─leaf 2", eventArgs [1].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ()); - Assert.Equal ("oot two", eventArgs [2].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ()); + Assert.Equal ("─leaf 1", eventArgs [0].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ()); + Assert.Equal ("─leaf 2", eventArgs [1].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ()); + Assert.Equal ("oot two", eventArgs [2].RuneCells.Aggregate ("", (s, n) => s += n.Rune).TrimEnd ()); - Assert.Equal (0, eventArgs [0].IndexOfExpandCollapseSymbol); - Assert.Equal (0, eventArgs [1].IndexOfExpandCollapseSymbol); - Assert.Null (eventArgs [2].IndexOfExpandCollapseSymbol); + Assert.Equal (0, eventArgs [0].IndexOfExpandCollapseSymbol); + Assert.Equal (0, eventArgs [1].IndexOfExpandCollapseSymbol); + Assert.Null (eventArgs [2].IndexOfExpandCollapseSymbol); - Assert.Equal (1, eventArgs [0].IndexOfModelText); - Assert.Equal (1, eventArgs [1].IndexOfModelText); - Assert.Equal (-1, eventArgs [2].IndexOfModelText); + Assert.Equal (1, eventArgs [0].IndexOfModelText); + Assert.Equal (1, eventArgs [1].IndexOfModelText); + Assert.Equal (-1, eventArgs [2].IndexOfModelText); - Assert.Equal ("leaf 1", eventArgs [0].Model.Text); - Assert.Equal ("leaf 2", eventArgs [1].Model.Text); - Assert.Equal ("root two", eventArgs [2].Model.Text); - } + Assert.Equal ("leaf 1", eventArgs [0].Model.Text); + Assert.Equal ("leaf 2", eventArgs [1].Model.Text); + Assert.Equal ("root two", eventArgs [2].Model.Text); + } - [Fact, AutoInitShutdown] - public void TestTreeView_DrawLineEvent_Handled () - { - var tv = new TreeView { Width = 20, Height = 10 }; + [Fact, AutoInitShutdown] + public void TestTreeView_DrawLineEvent_Handled () + { + var tv = new TreeView { Width = 20, Height = 10 }; - tv.DrawLine += (s, e) => { - if(e.Model.Text.Equals("leaf 1")) { - e.Handled = true; + tv.DrawLine += (s, e) => { + if (e.Model.Text.Equals ("leaf 1")) { + e.Handled = true; - for (int i = 0; i < 10; i++) { + for (var i = 0; i < 10; i++) { - e.Tree.AddRune (i,e.Y,new System.Text.Rune('F')); - } + e.Tree.AddRune (i, e.Y, new Rune ('F')); } - }; - - var n1 = new TreeNode ("root one"); - var n1_1 = new TreeNode ("leaf 1"); - var n1_2 = new TreeNode ("leaf 2"); - n1.Children.Add (n1_1); - n1.Children.Add (n1_2); - - var n2 = new TreeNode ("root two"); - tv.AddObject (n1); - tv.AddObject (n2); - tv.Expand (n1); - - tv.ColorScheme = new ColorScheme (); - tv.LayoutSubviews (); - tv.Draw (); - - // Normal drawing of the tree view - TestHelpers.AssertDriverContentsAre ( -@" + } + }; + + var n1 = new TreeNode ("root one"); + var n1_1 = new TreeNode ("leaf 1"); + var n1_2 = new TreeNode ("leaf 2"); + n1.Children.Add (n1_1); + n1.Children.Add (n1_2); + + var n2 = new TreeNode ("root two"); + tv.AddObject (n1); + tv.AddObject (n2); + tv.Expand (n1); + + tv.ColorScheme = new ColorScheme (); + tv.LayoutSubviews (); + tv.Draw (); + + // Normal drawing of the tree view + TestHelpers.AssertDriverContentsAre ( + @" ├-root one FFFFFFFFFF │ └─leaf 2 └─root two -", output); - } - - [Fact, AutoInitShutdown] - public void TestTreeView_Filter () - { - var tv = new TreeView { Width = 20, Height = 10 }; - - var n1 = new TreeNode ("root one"); - var n1_1 = new TreeNode ("leaf 1"); - var n1_2 = new TreeNode ("leaf 2"); - n1.Children.Add (n1_1); - n1.Children.Add (n1_2); - - var n2 = new TreeNode ("root two"); - tv.AddObject (n1); - tv.AddObject (n2); - tv.Expand (n1); - - tv.ColorScheme = new ColorScheme (); - tv.LayoutSubviews (); - tv.Draw (); - - // Normal drawing of the tree view - TestHelpers.AssertDriverContentsAre ( -@" +", _output); + } + + [Fact, AutoInitShutdown] + public void TestTreeView_Filter () + { + var tv = new TreeView { Width = 20, Height = 10 }; + + var n1 = new TreeNode ("root one"); + var n1_1 = new TreeNode ("leaf 1"); + var n1_2 = new TreeNode ("leaf 2"); + n1.Children.Add (n1_1); + n1.Children.Add (n1_2); + + var n2 = new TreeNode ("root two"); + tv.AddObject (n1); + tv.AddObject (n2); + tv.Expand (n1); + + tv.ColorScheme = new ColorScheme (); + tv.LayoutSubviews (); + tv.Draw (); + + // Normal drawing of the tree view + TestHelpers.AssertDriverContentsAre ( + @" ├-root one │ ├─leaf 1 │ └─leaf 2 └─root two -", output); - var filter = new TreeViewTextFilter (tv); - tv.Filter = filter; - - // matches nothing - filter.Text = "asdfjhasdf"; - tv.Draw (); - // Normal drawing of the tree view - TestHelpers.AssertDriverContentsAre ( -@"", output); - - - // Matches everything - filter.Text = "root"; - tv.Draw (); - TestHelpers.AssertDriverContentsAre ( -@" +", _output); + var filter = new TreeViewTextFilter (tv); + tv.Filter = filter; + + // matches nothing + filter.Text = "asdfjhasdf"; + tv.Draw (); + // Normal drawing of the tree view + TestHelpers.AssertDriverContentsAre ( + @"", _output); + + + // Matches everything + filter.Text = "root"; + tv.Draw (); + TestHelpers.AssertDriverContentsAre ( + @" ├-root one │ ├─leaf 1 │ └─leaf 2 └─root two -", output); - // Matches 2 leaf nodes - filter.Text = "leaf"; - tv.Draw (); - TestHelpers.AssertDriverContentsAre ( -@" +", _output); + // Matches 2 leaf nodes + filter.Text = "leaf"; + tv.Draw (); + TestHelpers.AssertDriverContentsAre ( + @" ├-root one │ ├─leaf 1 │ └─leaf 2 -", output); +", _output); - // Matches 1 leaf nodes - filter.Text = "leaf 1"; - tv.Draw (); - TestHelpers.AssertDriverContentsAre ( -@" + // Matches 1 leaf nodes + filter.Text = "leaf 1"; + tv.Draw (); + TestHelpers.AssertDriverContentsAre ( + @" ├-root one │ ├─leaf 1 -", output); - } - - [Fact, AutoInitShutdown] - public void DesiredCursorVisibility_MultiSelect () - { - var tv = new TreeView { Width = 20, Height = 10 }; - - var n1 = new TreeNode ("normal"); - var n2 = new TreeNode ("pink"); - tv.AddObject (n1); - tv.AddObject (n2); - - Application.Top.Add (tv); - Application.Begin (Application.Top); - - Assert.True (tv.MultiSelect); - Assert.True (tv.HasFocus); - Assert.Equal (CursorVisibility.Invisible, tv.DesiredCursorVisibility); - - tv.SelectAll (); - tv.DesiredCursorVisibility = CursorVisibility.Default; - Application.Refresh (); - Application.Driver.GetCursorVisibility (out CursorVisibility visibility); - Assert.Equal (CursorVisibility.Default, tv.DesiredCursorVisibility); - Assert.Equal (CursorVisibility.Default, visibility); - } - - /// - /// Test object which considers for equality only - /// - private class EqualityTestObject { - public string Name { get; set; } - public int Age { get; set; } - - public override int GetHashCode () - { - return Name?.GetHashCode () ?? base.GetHashCode (); - } - public override bool Equals (object obj) - { - return obj is EqualityTestObject eto && Equals (Name, eto.Name); - } - } - - private void InitFakeDriver () - { - var driver = new FakeDriver (); - Application.Init (driver); - driver.Init (); - } +", _output); + } + + [Fact, AutoInitShutdown] + public void DesiredCursorVisibility_MultiSelect () + { + var tv = new TreeView { Width = 20, Height = 10 }; + + var n1 = new TreeNode ("normal"); + var n2 = new TreeNode ("pink"); + tv.AddObject (n1); + tv.AddObject (n2); + + Application.Top.Add (tv); + Application.Begin (Application.Top); + + Assert.True (tv.MultiSelect); + Assert.True (tv.HasFocus); + Assert.Equal (CursorVisibility.Invisible, tv.DesiredCursorVisibility); + + tv.SelectAll (); + tv.DesiredCursorVisibility = CursorVisibility.Default; + Application.Refresh (); + Application.Driver.GetCursorVisibility (out var visibility); + Assert.Equal (CursorVisibility.Default, tv.DesiredCursorVisibility); + Assert.Equal (CursorVisibility.Default, visibility); + } + + void InitFakeDriver () + { + var driver = new FakeDriver (); + Application.Init (driver); + driver.Init (); + } + + /// + /// Test object which considers for equality only + /// + class EqualityTestObject { + public string Name { get; set; } + + public int Age { get; set; } + + public override int GetHashCode () => Name?.GetHashCode () ?? base.GetHashCode (); + + public override bool Equals (object obj) => obj is EqualityTestObject eto && Equals (Name, eto.Name); + } + + #region Test Setup Methods + class Factory { + public Car [] Cars { get; set; } + + public override string ToString () => "Factory"; + } + + class Car { + public string Name { get; set; } + + public override string ToString () => Name; + } + + TreeView CreateTree () => CreateTree (out _, out _, out _); + + TreeView CreateTree (out Factory factory1, out Car car1, out Car car2) + { + car1 = new Car (); + car2 = new Car (); + + factory1 = new Factory { + Cars = new [] { car1, car2 } + }; + + var tree = new TreeView (new DelegateTreeBuilder (s => s is Factory f ? f.Cars : null)); + tree.AddObject (factory1); + + return tree; } -} + #endregion +} \ No newline at end of file