XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 0
UTF-8
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;

/// <summary>
/// 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.
/// </summary>
public partial class BlueprintPropertyViewData
{
    public object? Value { get; set; }

    public string ValueString => FormatValue(Value);

    public object? CustomData { get; set; }

    public Type? Type { get; set; }

    /// <summary>The exact CLR field/property name. Never translate this value.</summary>
    public string? Name { get; set; }

    /// <summary>An optional label for collection entries and definition objects.</summary>
    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; }

    /// <summary>
    /// The setter created while reflecting this member. It deliberately does not rely on
    /// <see cref="Name"/>, because collection entries and translated labels are not CLR members.
    /// </summary>
    internal Action<object?>? 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<BlueprintPropertyViewData> 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<object>()
                         .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<CheckBox>())
                {
                    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<IMessageService>()?.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))}>";
    }
}