StreamingLevelFilterWindow

This commit is contained in:
Asval 2026-06-27 21:16:24 +02:00
parent 1da370d1f4
commit e869feb482
6 changed files with 473 additions and 3 deletions

@ -1 +1 @@
Subproject commit 8d145bb84efee7dfc1efe9ed2b2d9e53d8b463c3
Subproject commit 068e843fb40649559d0aa82e5f2767b2e6a42512

View File

@ -14,6 +14,7 @@ using CUE4Parse.Utils;
using FModel.Extensions;
using FModel.Framework;
using FModel.Settings;
using FModel.Views;
using FModel.Views.Snooper;
using Serilog.Events;
@ -58,7 +59,16 @@ public class ExportSessionViewModel : ViewModel
get
{
if (_session != null) return _session;
_session = new ExportSession();
_session = new ExportSession((args, ct) =>
{
Application.Current.Dispatcher.Invoke(() =>
{
var window = new StreamingLevelFilterWindow(new StreamingLevelFilterViewModel(args));
_stopwatch.Stop();
window.ShowDialog();
_stopwatch.Start();
}, DispatcherPriority.Normal, ct);
});
_session.PropertyChanged += OnSessionPropertyChanged;
return _session;
}

View File

@ -0,0 +1,192 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using CUE4Parse_Conversion;
using CUE4Parse_Conversion.Dto;
namespace FModel.ViewModels;
public class StreamingLevelFilterViewModel
{
public string WorldName { get; }
public int TotalCount { get; }
public List<ActorNodeVm> Children { get; } = [];
public StreamingLevelFilterViewModel(StreamingLevelFilterArgs args)
{
WorldName = args.WorldName;
foreach (var actor in args.Actors)
{
Children.Add(new ActorNodeVm(actor));
}
var worldLevels = new ActorNodeVm("Streaming Levels") { IsExpanded = true };
foreach (var level in args.StreamingLevels)
{
worldLevels.Children.Add(new StreamingLevelNodeVm(level));
}
if (worldLevels.Children.Count > 0) Children.Add(worldLevels);
TotalCount = CountLevels(args.Actors) + args.StreamingLevels.Count;
}
public void SkipAll()
{
foreach (var child in Children)
{
child.IsChecked = false;
}
}
private int CountLevels(IReadOnlyList<ActorDto> actors)
{
var n = 0;
foreach (var a in actors) n += CountFromActor(a);
return n;
}
private int CountFromActor(ActorDto actor)
{
var n = actor.StreamingLevels?.Count ?? 0;
if (actor.RootComponent is { } comp) n += CountFromComponent(comp);
return n;
}
private int CountFromComponent(SceneComponentDto comp)
{
var n = 0;
foreach (var a in comp.AttachedActors) n += CountFromActor(a);
foreach (var c in comp.Children) n += CountFromComponent(c);
return n;
}
}
public class ActorNodeVm : TreeNodeVm
{
public override string Name { get; }
public bool IsExpanded { get; set; }
public ObservableCollection<TreeNodeVm> Children { get; } = [];
private static int _batch;
public ActorNodeVm(string name)
{
Name = name;
Children.CollectionChanged += (_, e) =>
{
if (e.NewItems is null) return;
foreach (TreeNodeVm child in e.NewItems)
{
child.PropertyChanged += (_, args) =>
{
if (args.PropertyName == nameof(IsChecked) && _batch == 0)
OnPropertyChanged(nameof(IsChecked));
};
}
};
}
public ActorNodeVm(ActorDto actor) : this(actor.Name)
{
if (actor.StreamingLevels is { Count: > 0 } streamingLevels)
foreach (var level in streamingLevels)
Children.Add(new StreamingLevelNodeVm(level));
CollectFromComponent(actor.RootComponent);
}
private ActorNodeVm(SceneComponentDto component) : this(component.Name)
{
CollectFromComponent(component);
}
private void NotifyIntermediates()
{
foreach (var child in Children.OfType<ActorNodeVm>())
{
child.NotifyIntermediates();
child.OnPropertyChanged(nameof(IsChecked));
}
}
private void CollectFromComponent(SceneComponentDto? comp)
{
if (comp is null) return;
foreach (var actor in comp.AttachedActors)
Children.Add(new ActorNodeVm(actor));
foreach (var component in comp.Children)
Children.Add(new ActorNodeVm(component));
}
private IEnumerable<StreamingLevelNodeVm> AllLevels()
{
foreach (var child in Children)
{
switch (child)
{
case StreamingLevelNodeVm sl:
yield return sl;
break;
case ActorNodeVm actor:
{
foreach (var l in actor.AllLevels())
{
yield return l;
}
break;
}
}
}
}
public bool? IsChecked
{
get
{
var all = AllLevels().ToList();
if (all.Count == 0) return false;
var trueCount = all.Count(l => l.IsChecked);
if (trueCount == all.Count) return true;
if (trueCount == 0) return false;
return null;
}
set
{
var v = value ?? false;
_batch++;
foreach (var l in AllLevels())
l.IsChecked = v;
NotifyIntermediates();
_batch--;
OnPropertyChanged();
}
}
}
public class StreamingLevelNodeVm(StreamingLevel level) : TreeNodeVm
{
public override string Name { get; } = level.World.Name;
public bool IsChecked
{
get => level.IsPersistent;
set
{
level.IsPersistent = value;
OnPropertyChanged();
}
}
}
public abstract class TreeNodeVm : INotifyPropertyChanged
{
public abstract string Name { get; }
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? name = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

View File

@ -5,7 +5,6 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" d:DesignWidth="512" d:DesignHeight="512"
d:DataContext="{d:DesignInstance Type=vm:ExportSessionViewModel, IsDesignTimeCreatable=False}"
Background="{DynamicResource {x:Static adonisUi:Brushes.Layer0BackgroundBrush}}"
xmlns:vm="clr-namespace:FModel.ViewModels"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"

View File

@ -0,0 +1,244 @@
<adonisControls:AdonisWindow x:Class="FModel.Views.StreamingLevelFilterWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="520"
d:DataContext="{d:DesignInstance Type=vm:StreamingLevelFilterViewModel, IsDesignTimeCreatable=False}"
xmlns:vm="clr-namespace:FModel.ViewModels"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
xmlns:adonisControls="clr-namespace:AdonisUI.Controls;assembly=AdonisUI"
xmlns:adonisExtensions="clr-namespace:AdonisUI.Extensions;assembly=AdonisUI"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
WindowStartupLocation="CenterScreen" ResizeMode="NoResize" IconVisibility="Collapsed"
Height="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenHeight}, Converter={converters:RatioConverter}, ConverterParameter='0.60'}"
Width="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.28'}">
<adonisControls:AdonisWindow.Style>
<Style TargetType="adonisControls:AdonisWindow" BasedOn="{StaticResource {x:Type adonisControls:AdonisWindow}}">
<Setter Property="Title" Value="Streamed Levels" />
</Style>
</adonisControls:AdonisWindow.Style>
<adonisControls:AdonisWindow.Resources>
<Style x:Key="LevelTreeViewItemStyle" TargetType="TreeViewItem" BasedOn="{StaticResource TreeViewItemStyle}">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TreeViewItem">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<Border x:Name="Border"
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding adonisExtensions:CornerRadiusExtension.CornerRadius}" />
<Border x:Name="SpotlightLayer"
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
Background="{TemplateBinding adonisExtensions:CursorSpotlightExtension.BackgroundBrush}"
BorderBrush="{TemplateBinding adonisExtensions:CursorSpotlightExtension.BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding adonisExtensions:CornerRadiusExtension.CornerRadius}"
adonisExtensions:CursorSpotlightExtension.MouseEventSource="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=TreeViewItem}}"
SnapsToDevicePixels="False" />
<ToggleButton x:Name="Expander"
Grid.Row="0" Grid.Column="0"
Width="16" Height="16"
Margin="8,0,8,0"
Focusable="False" ClickMode="Press"
Foreground="{TemplateBinding Foreground}"
IsChecked="{Binding IsExpanded, RelativeSource={RelativeSource TemplatedParent}}"
RenderTransformOrigin="0.5,0.5">
<ToggleButton.Template>
<ControlTemplate TargetType="ToggleButton">
<Viewbox Width="16" Height="16" HorizontalAlignment="Center">
<Canvas Width="24" Height="24" Background="Transparent">
<Path Fill="{TemplateBinding Foreground}"
Data="{StaticResource ArrowIcon}" />
</Canvas>
</Viewbox>
</ControlTemplate>
</ToggleButton.Template>
<ToggleButton.RenderTransform>
<RotateTransform x:Name="ExpanderRotateTransform" Angle="-90" />
</ToggleButton.RenderTransform>
</ToggleButton>
<ContentPresenter x:Name="PART_Header"
Grid.Row="0" Grid.Column="1"
ContentSource="Header"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
Margin="0,4,8,4" />
<ItemsPresenter x:Name="ItemsHost"
Grid.Row="1" Grid.Column="1" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="False">
<Setter TargetName="ItemsHost" Property="Visibility" Value="Collapsed" />
</Trigger>
<Trigger Property="IsExpanded" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="ExpanderRotateTransform"
Storyboard.TargetProperty="Angle"
Duration="0:0:0.2" To="0" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="ExpanderRotateTransform"
Storyboard.TargetProperty="Angle"
Duration="0:0:0.2" From="0" />
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
<Trigger Property="HasItems" Value="False">
<Setter TargetName="Expander" Property="Visibility" Value="Hidden" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</adonisControls:AdonisWindow.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="10 10 10 0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<DockPanel Grid.Row="0">
<Grid DockPanel.Dock="Left" VerticalAlignment="Center" HorizontalAlignment="Left">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0"
Text="{Binding TotalCount, Mode=OneWay, FallbackValue=0}"
FontSize="20" FontWeight="SemiBold"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="streamed levels" FontSize="10"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.DisabledForegroundBrush}}" />
<Rectangle Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Width="1" Margin="16 2"
Fill="{DynamicResource {x:Static adonisUi:Brushes.DisabledForegroundBrush}}" />
<TextBlock Grid.Row="0" Grid.RowSpan="2" Grid.Column="2" TextWrapping="Wrap" FontSize="10" VerticalAlignment="Center"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.DisabledForegroundBrush}}">
<Run Text="{Binding WorldName, Mode=OneWay, StringFormat='{}{0} loads these levels at runtime, select which ones to export.'}"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}"/>
<LineBreak />
<Run Text="Unchecked levels will be referenced but not exported." />
<LineBreak />
<Run Text="Skip to ignore all of them." />
</TextBlock>
</Grid>
</DockPanel>
<Separator Grid.Row="1" Style="{StaticResource CustomSeparator}" Margin="0 5" />
<TreeView Grid.Row="2" ItemsSource="{Binding Children}"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling"
VirtualizingPanel.ScrollUnit="Item"
ScrollViewer.CanContentScroll="True"
ItemContainerStyle="{StaticResource LevelTreeViewItemStyle}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type vm:ActorNodeVm}" ItemsSource="{Binding Children}">
<Grid VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<CheckBox Grid.Column="0" IsChecked="{Binding IsChecked, Mode=TwoWay}"
IsThreeState="True"
VerticalAlignment="Center" Margin="0 0 5 0" />
<TextBlock Grid.Column="1" Text="{Binding Name}" VerticalAlignment="Center" />
<TextBlock Grid.Column="2" Margin="5 0 0 0" VerticalAlignment="Center"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.DisabledForegroundBrush}}"
Text="{Binding Children.Count, Mode=OneWay, StringFormat='({0})'}" />
</Grid>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type vm:StreamingLevelNodeVm}">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<CheckBox IsChecked="{Binding IsChecked, Mode=TwoWay}"
VerticalAlignment="Center" Margin="0 0 5 0" />
<TextBlock Text="{Binding Name}" VerticalAlignment="Center" />
</StackPanel>
</DataTemplate>
</TreeView.Resources>
</TreeView>
</Grid>
<Border Grid.Row="1"
Background="{DynamicResource {x:Static adonisUi:Brushes.Layer1BackgroundBrush}}"
adonisExtensions:LayerExtension.IncreaseLayer="True">
<Grid Margin="30, 12, 6, 12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" MinWidth="78" Margin="0 0 12 0" IsDefault="False" IsCancel="True"
HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Skip" Click="OnSkipClick"
Style="{DynamicResource {x:Static adonisUi:Styles.AccentButton}}">
<Button.Resources>
<SolidColorBrush x:Key="{x:Static adonisUi:Brushes.AccentBrush}" Color="#D49220" />
<SolidColorBrush x:Key="{x:Static adonisUi:Brushes.AccentHighlightBrush}" Color="#E0A030" />
<SolidColorBrush x:Key="{x:Static adonisUi:Brushes.AccentIntenseHighlightBrush}" Color="#E0A030" />
<SolidColorBrush x:Key="{x:Static adonisUi:Brushes.AccentInteractionBrush}" Color="#B87C10" />
<SolidColorBrush x:Key="{x:Static adonisUi:Brushes.AccentInteractionBorderBrush}" Color="#9A6800" />
</Button.Resources>
</Button>
<Button Grid.Column="2" MinWidth="78" Margin="0 0 12 0" IsDefault="False" IsCancel="False"
HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="OK"
Click="OnOkClick" />
</Grid>
</Border>
</Grid>
</adonisControls:AdonisWindow>

View File

@ -0,0 +1,25 @@
using System.Windows;
using System.Windows.Controls;
using FModel.ViewModels;
namespace FModel.Views;
public partial class StreamingLevelFilterWindow
{
public StreamingLevelFilterWindow(StreamingLevelFilterViewModel vm)
{
InitializeComponent();
DataContext = vm;
}
private void OnOkClick(object sender, RoutedEventArgs e) => Close();
private void OnSkipClick(object sender, RoutedEventArgs e)
{
if (sender is Button { DataContext: StreamingLevelFilterViewModel viewModel })
{
viewModel.SkipAll();
Close();
}
}
}