using System.Text.RegularExpressions; using Sandbox.Internal; namespace Sandbox { /// /// Collects all the relevant info (such as description, name, icon, etc) from attributes and other sources about a type or type member. /// public partial struct DisplayInfo : ITitleProvider, IDescriptionProvider, ICategoryProvider, IIconProvider, IOrderProvider { /// /// "Internal" class name of this type or member. This typically should be all lowercase and without weird symbols or whitespace. /// public string ClassName; /// /// Namespace of this type /// public string Namespace; /// /// Namespace.ParentClass.Class.Member /// public string Fullname; /// /// The name of this type or member. /// public string Name; /// /// The summary or description of this type or member. /// public string Description; /// /// Group or category of this type or member. () /// public string Group; /// /// This is marked as ReadOnly /// public bool ReadOnly; /// /// Material icon of this type or member. () /// public string Icon; /// /// Order of this member for UI ordering purposes. () /// public int Order; /// /// Whether this member should be visible in a properties sheet () /// public bool Browsable; /// /// Placeholder text for string type properties. () /// Placeholder text is displayed in UI when input text field is empty. /// public string Placeholder; /// /// Possible aliases for this type or member, if any. () /// public string[] Alias; /// /// Tags of this type or member. () /// public string[] Tags; /// /// Returns whether this type or member has given tag. () /// /// The tag to test. /// Whether the tag is present or not public bool HasTag( string t ) => Tags?.Contains( t ) ?? false; /// /// Retrieves display info about a given type. /// /// The type to look up display info for. /// Whether to load in base type's display info first, then overrides all possible fields with given type's information. /// The display info. Will contain empty fields on failure. public static DisplayInfo ForType( System.Type t, bool inherit = true ) => ForMember( t, inherit ); /// /// Retrieves display info about a given objects type. /// /// The type to look up display info for. /// Whether to load in base type's display info first, then overrides all possible fields with given type's information. /// The display info. Will contain empty fields on failure. public static DisplayInfo For( object t, bool inherit = true ) => ForMember( t?.GetType(), inherit ); /// /// Retrieves display info about a given member or type. /// /// The member to look up display info for. /// If member given is a , loads in base type's display info first, then overrides all possible fields with given type's information. /// The display info. Will contain empty fields on failure. public static DisplayInfo ForMember( MemberInfo t, bool inherit = true ) => ForMember( t, inherit, null ); private static Dictionary SystemTypeAliases { get; } = new() { { typeof(bool), "bool" }, { typeof(byte), "byte" }, { typeof(char), "char" }, { typeof(decimal), "decimal" }, { typeof(double), "double" }, { typeof(float), "float" }, { typeof(int), "int" }, { typeof(long), "long" }, { typeof(object), "object" }, { typeof(sbyte), "sbyte" }, { typeof(short), "short" }, { typeof(string), "string" }, { typeof(uint), "uint" }, { typeof(ulong), "ulong" }, { typeof(ushort), "ushort" }, { typeof(void), "void" } }; private static Dictionary SystemTypeIcons { get; } = new() { { typeof(string), "format_quote" } }; internal static DisplayInfo ForMember( MemberInfo t, bool inherit, IEnumerable cachedAttributes ) { if ( t == null ) return default; DisplayInfo info = default; if ( inherit && t is System.Type type && type.BaseType != null ) { info = ForMember( type.BaseType, true ); } // Inherit member descriptions from interfaces if ( inherit && t is not System.Type && t.DeclaringType != null ) { var member = t.DeclaringType.GetInterfaces().SelectMany( x => x.GetMember( t.Name, t.MemberType, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public ) ).FirstOrDefault(); if ( member != null ) info = ForMember( member, true ); } HashSet aliases = null; HashSet tags = null; var name = t.Name; var backQuoteIndex = name.IndexOf( '`' ); if ( backQuoteIndex != -1 ) { name = name[..backQuoteIndex]; } if ( t is Type { IsInterface: true } && IsInterfaceTypeName( name ) ) { name = name[1..]; } if ( t is MethodInfo { DeclaringType.IsInterface: true } && IsEventMethodName( name ) ) { info.Icon = "bolt"; } info.Name = name.ToTitleCase(); info.ClassName = t.Name; info.Browsable = true; info.Alias = Array.Empty(); if ( t is System.Type infoType ) { if ( SystemTypeAliases.TryGetValue( infoType, out var alias ) ) { info.Name = alias; } if ( SystemTypeIcons.TryGetValue( infoType, out var icon ) ) { info.Icon = icon; } info.Namespace = infoType.Namespace; info.Fullname = infoType.FullName; } if ( cachedAttributes == null ) cachedAttributes = t.GetCustomAttributes( false ).Cast(); foreach ( var attribute in cachedAttributes ) { if ( attribute is System.ComponentModel.DataAnnotations.DisplayAttribute display ) { if ( !string.IsNullOrWhiteSpace( display.Name ) ) info.Name = display.Name; if ( !string.IsNullOrWhiteSpace( display.GroupName ) ) info.Group = display.GroupName; if ( !string.IsNullOrWhiteSpace( display.Description ) ) info.Description = display.Description; info.Order = display.GetOrder() ?? info.Order; } if ( attribute is AliasAttribute aa ) { foreach ( var n in aa.Value ) { aliases ??= new(); aliases.Add( n.ToLower() ); } } if ( attribute is TagAttribute ta ) { foreach ( var n in ta.EnumerateValues() ) { tags ??= new(); tags.Add( n.ToLower() ); } } if ( attribute is ITitleProvider titleProvider ) info.Name = titleProvider.Value ?? info.Name; if ( attribute is IClassNameProvider classnameProvider ) info.ClassName = classnameProvider.Value ?? info.ClassName; if ( attribute is IDescriptionProvider descriptionProvider ) info.Description = descriptionProvider.Value ?? info.Description; if ( attribute is IPlaceholderProvider placeholderProvider ) info.Placeholder = placeholderProvider.Value ?? info.Placeholder; if ( attribute is ICategoryProvider categoryProvider ) info.Group = categoryProvider.Value ?? info.Group; if ( attribute is HideAttribute ) info.Browsable = false; if ( attribute is System.ComponentModel.CategoryAttribute categoryAttribute ) info.Group = categoryAttribute.Category ?? info.Group; if ( attribute is IIconProvider iconAttribute ) info.Icon = iconAttribute.Value ?? info.Icon; if ( attribute is ReadOnlyAttribute ) info.ReadOnly = true; if ( attribute is OrderAttribute o ) info.Order = o.Value; } if ( aliases != null ) { info.Alias = aliases.ToArray(); } if ( tags != null ) { info.Tags = tags.ToArray(); } return info; } /// /// Returns display info for each member of an enumeration type. /// public static DisplayInfo[] ForEnumValues( System.Type t ) { // // TODO: cache me daddy // var names = t.GetEnumNames(); return names.Select( x => ForMember( t.GetMember( x, BindingFlags.Public | BindingFlags.Static ) .First( m => m.MemberType == MemberTypes.Field ) ) ) .ToArray(); } /// /// Returns display info for each member of an enumeration type. /// public static (T value, DisplayInfo info)[] ForEnumValues() where T : Enum { // // TODO: cache me daddy // var t = typeof( T ); var names = t.GetEnumNames(); var values = t.GetEnumValues(); return Enumerable.Range( 0, names.Length ) .Select( i => new( (T)values.GetValue( i ), ForMember( typeof( T ) .GetMember( names[i], BindingFlags.Public | BindingFlags.Static ) .First( m => m.MemberType == MemberTypes.Field ) ) ) ) .ToArray(); } [GeneratedRegex( @"^I[A-Z]" )] private static partial Regex InterfaceTypeNameRegex(); [GeneratedRegex( @"^On[A-Z]" )] private static partial Regex EventMethodNameRegex(); private static bool IsInterfaceTypeName( string name ) { return InterfaceTypeNameRegex().IsMatch( name ); } private static bool IsEventMethodName( string name ) { return EventMethodNameRegex().IsMatch( name ); } readonly string ITitleProvider.Value => Name; readonly string IDescriptionProvider.Value => Description; readonly string ICategoryProvider.Value => Group; readonly string IIconProvider.Value => Icon; readonly int IOrderProvider.Value => Order; } }