using Microsoft.UI.Xaml.Media; using SpaceEngineersBlueprintEditor.Utilities.Localization; using System.Collections; using System.Globalization; using XFEExtension.NetCore.WinUIHelper.Interface.Services; using XFEExtension.NetCore.WinUIHelper.Utilities; using XFEExtension.NetCore.XFETransform; namespace SpaceEngineersBlueprintEditor.Model; /// /// A property node used by both blueprint property trees. /// The original member name, translated display name and value setter are kept separate /// so presentation never interferes with reflection or serialization. /// public partial class BlueprintPropertyViewData { public object? Value { get; set; } public string ValueString => FormatValue(Value); public object? CustomData { get; set; } public Type? Type { get; set; } /// The exact CLR field/property name. Never translate this value. public string? Name { get; set; } /// An optional label for collection entries and definition objects. public string? DisplayNameOverride { get; set; } public string DisplayName => DisplayNameOverride ?? BlueprintPropertyNameLocalizer.GetDisplayName(Name); public string FriendlyTypeName => GetFriendlyTypeName(Type); public string NameTypeString => string.IsNullOrEmpty(FriendlyTypeName) ? DisplayName : $"{DisplayName} ยท {FriendlyTypeName}"; public string[] EnumValues => EffectiveType?.IsEnum == true ? Enum.GetNames(EffectiveType) : []; public BlueprintPropertyViewData? Parent { get; set; } public ImageSource? CubeImage { get; set; } /// /// The setter created while reflecting this member. It deliberately does not rely on /// , because collection entries and translated labels are not CLR members. /// internal Action? ValueSetter { get; set; } internal Action? ValueChanged { get; set; } public bool CanWrite => ValueSetter is not null; public bool HasError { get; set; } public string? ErrorMessage { get; set; } public bool IsAnalyzed { get; set; } public List Children { get; } = []; public Type? EffectiveType => Type is null ? null : Nullable.GetUnderlyingType(Type) ?? Type; public bool IsNullable => Type is not null && (!Type.IsValueType || Nullable.GetUnderlyingType(Type) is not null); public bool IsEnumerable => Type is not null && Type != typeof(string) && typeof(IEnumerable).IsAssignableFrom(Type); public bool IsMultiEnum => EffectiveType?.IsEnum == true && EffectiveType.IsDefined(typeof(FlagsAttribute), false); public bool IsBasicType { get { if (EffectiveType is not { } type) return false; return XFEConverter.IsBasicType(type) || type.IsEnum || type == typeof(decimal) || type == typeof(DateTime) || type == typeof(DateTimeOffset) || type == typeof(TimeSpan) || type == typeof(Guid); } } public bool CanExpand => !IsBasicType && Type is not null && Value is not null && !HasError; public object? BestControl { get { if (!IsBasicType) return null; if (!CanWrite) return ReadOnlyControl; if (IsMultiEnum) return MultiEnumControl; if (EffectiveType?.IsEnum == true) return EnumControl; if (EffectiveType == typeof(bool)) return BoolControl; return TextControl; } } public TextBlock ReadOnlyControl { get { var text = HasError ? ErrorMessage : ValueString; var textBlock = new TextBlock { Text = text, TextTrimming = TextTrimming.CharacterEllipsis, IsTextSelectionEnabled = true, VerticalAlignment = VerticalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Stretch }; if (HasError) textBlock.Foreground = new SolidColorBrush(Microsoft.UI.Colors.OrangeRed); ToolTipService.SetToolTip(textBlock, text); return textBlock; } } public TextBox TextControl { get { var textBox = new TextBox { Text = ValueString, HorizontalAlignment = HorizontalAlignment.Stretch, MinWidth = 140 }; ToolTipService.SetToolTip(textBox, ValueString); textBox.LostFocus += (_, _) => CommitText(textBox); return textBox; } } public ComboBox EnumControl { get { var values = IsNullable ? new[] { string.Empty }.Concat(EnumValues).ToArray() : EnumValues; var comboBox = new ComboBox { ItemsSource = values, SelectedItem = Value?.ToString() ?? string.Empty, HorizontalAlignment = HorizontalAlignment.Stretch, MinWidth = 140 }; comboBox.SelectionChanged += (_, args) => { if (args.AddedItems.FirstOrDefault() is not string value || EffectiveType is null) return; if (string.IsNullOrEmpty(value) && IsNullable) SetValue(null); else SetValue(Enum.Parse(EffectiveType, value, true)); }; return comboBox; } } public SplitButton MultiEnumControl { get { var enumType = EffectiveType!; var currentBits = ToUInt64(Value); var stackPanel = new StackPanel { Spacing = 4 }; foreach (var enumValue in Enum.GetValues(enumType).Cast() .Where(IsSingleFlagValue)) { var bits = ToUInt64(enumValue); stackPanel.Children.Add(new CheckBox { Content = enumValue.ToString(), Tag = bits, IsChecked = bits == 0 ? currentBits == 0 : (currentBits & bits) == bits }); } var splitButton = new SplitButton { Content = ValueString, HorizontalAlignment = HorizontalAlignment.Stretch, MinWidth = 140, Flyout = new Flyout { Content = stackPanel } }; splitButton.Flyout.Closed += (_, _) => { ulong selectedBits = 0; foreach (var checkBox in stackPanel.Children.OfType()) { if (checkBox.IsChecked == true && checkBox.Tag is ulong bits && bits != 0) selectedBits |= bits; } var targetValue = Enum.ToObject(enumType, selectedBits); if (SetValue(targetValue)) splitButton.Content = targetValue.ToString(); }; return splitButton; } } public ComboBox BoolControl { get { var values = IsNullable ? new[] { string.Empty, bool.TrueString, bool.FalseString } : new[] { bool.TrueString, bool.FalseString }; var comboBox = new ComboBox { ItemsSource = values, SelectedItem = Value?.ToString() ?? string.Empty, HorizontalAlignment = HorizontalAlignment.Stretch, MinWidth = 140 }; comboBox.SelectionChanged += (_, args) => { if (args.AddedItems.FirstOrDefault() is not string value) return; if (string.IsNullOrEmpty(value) && IsNullable) SetValue(null); else if (bool.TryParse(value, out var parsed)) SetValue(parsed); }; return comboBox; } } public bool MatchesSearch(string? searchText) { if (string.IsNullOrWhiteSpace(searchText)) return true; var searchableText = string.Join( '\n', Name, DisplayName, BlueprintPropertyNameLocalizer.GetChineseName(Name), FriendlyTypeName, ValueString, CustomData as string, ErrorMessage); return searchText .Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .All(term => searchableText.Contains(term, StringComparison.OrdinalIgnoreCase)); } public bool SetValue(object? targetValue) { if (ValueSetter is null) return false; try { ValueSetter(targetValue); Value = targetValue; Parent?.PropagateBoxedValueType(); ValueChanged?.Invoke(); return true; } catch (Exception ex) { ShowSetValueError(targetValue, ex.Message); return false; } } private void CommitText(TextBox textBox) { if (textBox.Text == ValueString) return; var enteredText = textBox.Text; if (TryConvertText(enteredText, out var targetValue, out var error)) { if (SetValue(targetValue)) ToolTipService.SetToolTip(textBox, ValueString); else textBox.Text = ValueString; } else { textBox.Text = ValueString; ShowSetValueError(enteredText, error); } } private bool TryConvertText(string text, out object? value, out string error) { value = null; error = string.Empty; if (EffectiveType is not { } type) { error = "Unknown property type."; return false; } if (type == typeof(string)) { value = text; return true; } if (string.IsNullOrWhiteSpace(text) && IsNullable) return true; try { if (type == typeof(char)) { if (text.Length != 1) throw new FormatException("A character value must contain exactly one character."); value = text[0]; } else if (type == typeof(Guid)) { value = Guid.Parse(text); } else if (type == typeof(TimeSpan)) { value = TimeSpan.Parse(text, CultureInfo.InvariantCulture); } else if (type == typeof(DateTime)) { value = DateTime.Parse(text, CultureInfo.CurrentCulture, DateTimeStyles.RoundtripKind); } else if (type == typeof(DateTimeOffset)) { value = DateTimeOffset.Parse(text, CultureInfo.CurrentCulture, DateTimeStyles.RoundtripKind); } else if (type.IsEnum) { value = Enum.Parse(type, text, true); } else { // Convert directly to the target type. In particular, never route Int64, // UInt64 or Decimal through double, which would silently lose blueprint IDs. try { value = Convert.ChangeType(text, type, CultureInfo.InvariantCulture); } catch (FormatException) { value = Convert.ChangeType(text, type, CultureInfo.CurrentCulture); } } return true; } catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException or ArgumentException) { error = ex.Message; return false; } } private void PropagateBoxedValueType() { if (Type?.IsValueType != true || ValueSetter is null) return; ValueSetter(Value); Parent?.PropagateBoxedValueType(); } private void ShowSetValueError(object? targetValue, string detail) { ServiceManager.GetGlobalService()?.ShowMessage( $"{"Error_CantSetValue_CantSetProperty".GetLocalized()}{Name}({FriendlyTypeName})" + $"{"Error_CantSetValue_ValueTo".GetLocalized()}{targetValue}: {detail}", "Error".GetLocalized(), InfoBarSeverity.Error); } private static bool IsSingleFlagValue(object value) { var bits = ToUInt64(value); return bits == 0 || (bits & (bits - 1)) == 0; } private static ulong ToUInt64(object? value) { if (value is null) return 0; try { return Convert.ToUInt64(value, CultureInfo.InvariantCulture); } catch (OverflowException) { return unchecked((ulong)Convert.ToInt64(value, CultureInfo.InvariantCulture)); } } private static string FormatValue(object? value) => value switch { null => string.Empty, DateTime dateTime => dateTime.ToString("O", CultureInfo.InvariantCulture), DateTimeOffset dateTimeOffset => dateTimeOffset.ToString("O", CultureInfo.InvariantCulture), IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture) ?? string.Empty, _ => value.ToString() ?? string.Empty }; private static string GetFriendlyTypeName(Type? type) { if (type is null) return string.Empty; var nullableType = Nullable.GetUnderlyingType(type); if (nullableType is not null) return $"{GetFriendlyTypeName(nullableType)}?"; if (!type.IsGenericType) return type.Name; var genericName = type.Name.Split('`')[0]; return $"{genericName}<{string.Join(", ", type.GetGenericArguments().Select(GetFriendlyTypeName))}>"; } }