XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEWarframe

【WinUI】基于Warframe游戏本体的公开服务器以及安装器

公开
关注 0 Fork 0 Star 0
UTF-8
using System.ComponentModel;
using CommunityToolkit.WinUI.Controls;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Automation;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media;

namespace XFEWarframe.Desktop.Editing;

/// <summary>Shared native SelectorBar, forms and commands; the two applications differ only in their API transport.</summary>
public sealed class EquipmentEditorView : Grid
{
    public EquipmentEditorModel? Model
    {
        get => (EquipmentEditorModel?)GetValue(ModelProperty);
        set => SetValue(ModelProperty, value);
    }
    public static readonly DependencyProperty ModelProperty = DependencyProperty.Register(nameof(Model), typeof(EquipmentEditorModel), typeof(EquipmentEditorView), new PropertyMetadata(null, (sender, args) => ((EquipmentEditorView)sender).Changed((EquipmentEditorModel?)args.OldValue)));
    public event EventHandler? BackRequested;
    public SelectorBar Selector { get; } = new() { Name = "EquipmentSelector", HorizontalAlignment = HorizontalAlignment.Left };
    private readonly Grid body = new();
    private readonly Dictionary<string, double> offsets = [];
    private readonly Dictionary<string, bool> expansion = [];
    private string? shownTabKey;
    private string? selectedAbilityConfig;
    private readonly InfoBar notice = new() { IsClosable = false, Severity = InfoBarSeverity.Warning };
    private readonly TextBlock title = new() { TextWrapping = TextWrapping.Wrap, FontSize = 30, FontWeight = Microsoft.UI.Text.FontWeights.SemiBold };
    private readonly TextBlock subtitle = new() { Opacity = .7, TextWrapping = TextWrapping.Wrap };
    private readonly TextBlock summary = new() { Opacity = .7, TextWrapping = TextWrapping.Wrap };
    private readonly Expander summaryBox;
    private readonly ProgressRing progress = new() { Width = 36, Height = 36, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center };
    private bool rebuilding;

    public EquipmentEditorView()
    {
        Padding = new Thickness(28, 24, 28, 24); RowSpacing = 16;
        RowDefinitions.Add(new() { Height = GridLength.Auto });
        RowDefinitions.Add(new() { Height = GridLength.Auto });
        RowDefinitions.Add(new() { Height = GridLength.Auto });
        RowDefinitions.Add(new() { Height = new GridLength(1, GridUnitType.Star) });
        var header = new Grid { ColumnSpacing = 16 };
        header.ColumnDefinitions.Add(new() { Width = new GridLength(1, GridUnitType.Star) });
        header.ColumnDefinitions.Add(new() { Width = GridLength.Auto });
        var labels = new StackPanel { Spacing = 6 }; labels.Children.Add(subtitle); labels.Children.Add(title);
        summaryBox = new Expander { Header = UiText.Translate("当前装备信息"), Content = summary, HorizontalAlignment = HorizontalAlignment.Stretch };
        labels.Children.Add(summaryBox); header.Children.Add(labels);
        var toolbar = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8, VerticalAlignment = VerticalAlignment.Top };
        var back = new Button { Content = UiText.Translate("返回列表") }; back.Click += (_, _) => BackRequested?.Invoke(this, EventArgs.Empty);
        var refresh = new Button { Content = UiText.Translate("刷新"), Name = "RefreshEquipment" };
        refresh.SetBinding(Button.CommandProperty, new Binding { Path = new PropertyPath("RefreshCommand") });
        toolbar.Children.Add(back); toolbar.Children.Add(refresh); Grid.SetColumn(toolbar, 1); header.Children.Add(toolbar);
        Children.Add(header); Grid.SetRow(notice, 1); Children.Add(notice);
        var selectionScroll = new ScrollViewer { Content = Selector, HorizontalScrollMode = ScrollMode.Enabled,
            HorizontalScrollBarVisibility = ScrollBarVisibility.Auto, VerticalScrollMode = ScrollMode.Disabled,
            VerticalScrollBarVisibility = ScrollBarVisibility.Disabled };
        Grid.SetRow(selectionScroll, 2); Children.Add(selectionScroll);
        Grid.SetRow(body, 3); Children.Add(body); Grid.SetRow(progress, 3); Children.Add(progress);
        Selector.SelectionChanged += (_, _) =>
        {
            if (rebuilding || Model is null || Selector.SelectedItem?.Tag is not EquipmentTab group) return;
            Model.SelectedTabKey = group.Key;
            ShowSection(group);
            Selector.SelectedItem.StartBringIntoView();
        };
        // Model and view belong to the same page; Changed detaches the old model when replaced.
        // Avoid losing subscriptions to spurious native Unloaded events during template transitions.
        Loaded += (_, _) => Update();
    }

    private void Changed(EquipmentEditorModel? previous)
    {
        if (previous is not null) previous.PropertyChanged -= ModelChanged;
        DataContext = Model;
        if (Model is not null) Model.PropertyChanged += ModelChanged;
        Update(); BuildTabs();
    }
    private void ModelChanged(object? sender, PropertyChangedEventArgs args)
    {
        Update();
        if (args.PropertyName == nameof(EquipmentEditorModel.Tabs)) BuildTabs();
    }
    private void Update()
    {
        title.Text = Model?.Title ?? ""; subtitle.Text = Model?.Subtitle ?? "";
        summary.Text = Model?.Summary ?? ""; summaryBox.Visibility = summary.Text.Length > 0 ? Visibility.Visible : Visibility.Collapsed;
        notice.Message = Model?.Message ?? ""; notice.IsOpen = Model?.HasMessage == true;
        Selector.IsEnabled = Model?.IsIdle == true;
        foreach (var section in body.Children.OfType<Control>()) section.IsEnabled = Selector.IsEnabled;
        progress.IsActive = Model?.IsBusy == true;
        progress.Visibility = progress.IsActive ? Visibility.Visible : Visibility.Collapsed;
    }
    private void BuildTabs()
    {
        rebuilding = true;
        try
        {
            RememberScroll(); body.Children.Clear(); Selector.SelectedItem = null; Selector.Items.Clear();
            foreach (var group in Model?.Tabs ?? [])
            {
                var item = new SelectorBarItem { Text = group.Title, Tag = group };
                AutomationProperties.SetAutomationId(item, "equipment-section-" + group.Key);
                Selector.Items.Add(item);
            }
            var selected = Selector.Items.FirstOrDefault(x => ((EquipmentTab)x.Tag).Key == Model?.SelectedTabKey) ?? Selector.Items.FirstOrDefault();
            if (selected is not null)
            {
                Selector.SelectedItem = selected;
                Model!.SelectedTabKey = ((EquipmentTab)selected.Tag).Key;
                ShowSection((EquipmentTab)selected.Tag);
            }
        }
        finally { rebuilding = false; }
    }
    private void ShowSection(EquipmentTab group)
    {
        RememberScroll();
        // Cache drafts in the model, not templated controls. Reattaching a cached ScrollViewer can
        // leave its SettingsExpander inputs detached from native Loaded/TextChanged notifications.
        var content = CreateContent(group);
        content.IsEnabled = Model?.IsIdle == true;
        shownTabKey = group.Key;
        content.Loaded += (_, _) => content.ChangeView(null, offsets.GetValueOrDefault(group.Key), null, true);
        body.Children.Clear();
        body.Children.Add(content);
    }
    private void RememberScroll()
    {
        if (shownTabKey is not null && body.Children.FirstOrDefault() is ScrollViewer previous) offsets[shownTabKey] = previous.VerticalOffset;
    }
    private ScrollViewer CreateContent(EquipmentTab group)
    {
        if (group.Key == "experimental-abilities" && group.Actions.Any(x => x.Fields.Count > 4)) return CreateAbilityBanks(group);
        var content = new StackPanel { Spacing = 12, Padding = new Thickness(0, 0, 12, 12) };
        foreach (var action in group.Actions)
        {
            var submit = new Button { Content = action.Fields.Count > 0 ? UiText.Translate("保存") : UiText.Translate("执行"), Command = Model!.SaveCommand, CommandParameter = action };
            AutomationProperties.SetAutomationId(submit, "equipment-save-" + action.Key);
            if (action.Fields.Count > 0) submit.Style = (Style)Application.Current.Resources["OrbitAccentButtonStyle"];
            if (action.Fields.Count == 0)
            {
                content.Children.Add(new SettingsCard { Header = action.Label, Description = action.Description, Content = submit });
                continue;
            }
            var buttons = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 };
            var reset = new Button { Content = UiText.Translate("重置输入"), Command = Model.ResetCommand, CommandParameter = action };
            AutomationProperties.SetAutomationId(reset, "equipment-reset-" + action.Key);
            buttons.Children.Add(reset); buttons.Children.Add(submit);
            var section = new SettingsExpander { Header = action.Label, Description = action.Description, Content = buttons,
                IsExpanded = expansion.GetValueOrDefault(action.Key, group.Key is not ("abilities" or "experimental-abilities") || action == group.Actions[0]), HorizontalAlignment = HorizontalAlignment.Stretch, Tag = action };
            section.RegisterPropertyChangedCallback(SettingsExpander.IsExpandedProperty, (_, _) => expansion[action.Key] = section.IsExpanded);
            AutomationProperties.SetAutomationId(section, "equipment-operation-" + action.Key);
            // SettingsExpander applies its item style directly to SettingsCard children.
            foreach (var card in EditorForm.CreateCards(action.Fields)) section.Items.Add(card);
            content.Children.Add(section);
        }
        return new ScrollViewer { Content = content, HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled, VerticalScrollBarVisibility = ScrollBarVisibility.Auto };
    }

    private ScrollViewer CreateAbilityBanks(EquipmentTab group)
    {
        var content = new StackPanel { Spacing = 12, Padding = new Thickness(0, 0, 12, 12) };
        var configs = new SelectorBar();
        AutomationProperties.SetAutomationId(configs, "equipment-ability-configs");
        var editor = new StackPanel { Spacing = 8 };
        content.Children.Add(configs); content.Children.Add(editor);
        foreach (var action in group.Actions) configs.Items.Add(new SelectorBarItem { Text = action.Label, Tag = action });
        configs.SelectionChanged += (_, _) =>
        {
            if (configs.SelectedItem?.Tag is not EquipmentAction action) return;
            selectedAbilityConfig = action.Key;
            editor.Children.Clear();
            var buttons = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 };
            var reset = new Button { Content = UiText.Translate("重置输入"), Command = Model!.ResetCommand, CommandParameter = action };
            var save = new Button { Content = UiText.Translate("保存"), Command = Model.SaveCommand, CommandParameter = action,
                Style = (Style)Application.Current.Resources["OrbitAccentButtonStyle"] };
            AutomationProperties.SetAutomationId(reset, "equipment-reset-" + action.Key);
            AutomationProperties.SetAutomationId(save, "equipment-save-" + action.Key);
            buttons.Children.Add(reset); buttons.Children.Add(save);
            editor.Children.Add(new SettingsCard { Header = action.Label, Description = action.Description, Content = buttons });
            for (var slot = 1; slot <= 4; slot++)
            {
                var prefix = "ability" + slot;
                var fields = action.Fields.Where(x => x.Key == prefix || x.Key.StartsWith(prefix + "_", StringComparison.Ordinal)).ToArray();
                var key = action.Key + ".slot" + slot;
                var section = new SettingsExpander { Header = UiText.Translate("技能槽") + " " + slot,
                    IsExpanded = expansion.GetValueOrDefault(key, slot == 1), HorizontalAlignment = HorizontalAlignment.Stretch };
                section.RegisterPropertyChangedCallback(SettingsExpander.IsExpandedProperty, (_, _) => expansion[key] = section.IsExpanded);
                AutomationProperties.SetAutomationId(section, "equipment-operation-" + key);
                foreach (var card in EditorForm.CreateCards(fields)) section.Items.Add(card);
                editor.Children.Add(section);
            }
        };
        configs.SelectedItem = configs.Items.FirstOrDefault(x => ((EquipmentAction)x.Tag).Key == selectedAbilityConfig) ?? configs.Items.FirstOrDefault();
        return new ScrollViewer { Content = content, HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled, VerticalScrollBarVisibility = ScrollBarVisibility.Auto };
    }
}