Rebuild the depot consist editor

#9 category minis in the class list; #10 coupler after the last car; #11 drag and drop from the browser and consist reorder; #12 fix forced headdriver (occupancy); #13 hide archival vehicles; #14 randomise order; #15 randomise orientation; #16 pick starting vehicle; #18 collapse multi-unit sets; texture randomiser and stock database.
This commit is contained in:
maj00r
2026-08-22 23:15:35 +02:00
parent 12b06a2689
commit 785b8c7afa
4 changed files with 1130 additions and 106 deletions

View File

@@ -24,6 +24,11 @@
<HeaderedContentControl Grid.RowSpan="2" Theme="{StaticResource GroupBox}"
Header="{Binding [VehicleDatabase], Source={x:Static starterNg:App.Loc}}">
<DockPanel LastChildFill="True">
<Button Name="textureBaseButton" DockPanel.Dock="Top" Classes="Basic" FontSize="11"
Margin="0,0,0,6" Padding="8,2" Cursor="Hand"
HorizontalAlignment="Left"
Click="TextureBaseButton_OnClick"
Content="{Binding [OpenTextureBase], Source={x:Static starterNg:App.Loc}}" />
<Grid DockPanel.Dock="Top" ColumnDefinitions="*,*" ColumnSpacing="6" Margin="0,0,0,6">
<ComboBox Name="categoryCombo" Grid.Column="0" HorizontalAlignment="Stretch"
PlaceholderText="{Binding [Category], Source={x:Static starterNg:App.Loc}}"
@@ -34,12 +39,25 @@
PointerWheelChanged="Combo_OnPointerWheel"
SelectionChanged="ClassCombo_OnSelectionChanged" />
</Grid>
<TextBox Name="searchBox" DockPanel.Dock="Top" Margin="0,0,0,8"
<TextBox Name="searchBox" DockPanel.Dock="Top" Margin="0,0,0,6"
Watermark="{Binding [Search], Source={x:Static starterNg:App.Loc}}"
TextChanged="SearchBox_OnTextChanged" />
<CheckBox Name="hideArchivalCheck" DockPanel.Dock="Top" Margin="0,0,0,8"
Content="{Binding [HideArchivalVehicles], Source={x:Static starterNg:App.Loc}}"
IsCheckedChanged="HideArchivalCheck_OnChanged" />
<!-- Mini preview of the selected entry + the single Add button.
Both are docked at the bottom, under the flat vehicle list. -->
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="4" Margin="0,4,0,0">
<Button Name="addAllCategoryButton" Classes="Flat" FontSize="11" Padding="6,2"
Cursor="Hand" Click="AddAllCategoryButton_OnClick"
Content="{Binding [AddAllCategory], Source={x:Static starterNg:App.Loc}}"
ToolTip.Tip="{Binding [TipAddAllCategory], Source={x:Static starterNg:App.Loc}}" />
<Button Name="addUniqueMmdButton" Classes="Flat" FontSize="11" Padding="6,2"
Cursor="Hand" Click="AddUniqueMmdButton_OnClick"
Content="{Binding [AddUniqueMmd], Source={x:Static starterNg:App.Loc}}"
ToolTip.Tip="{Binding [TipAddUniqueMmd], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
<Button Name="addVehicleButton" DockPanel.Dock="Bottom" Classes="Flat Accent"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
Margin="0,8,0,0" IsEnabled="False" Cursor="Hand"
@@ -48,16 +66,19 @@
<Border Name="miniPreviewPanel" DockPanel.Dock="Bottom" Height="62" Margin="0,6,0,0"
Background="{StaticResource CarbonInputBg}"
BorderBrush="{StaticResource CarbonBorderLight}" BorderThickness="1"
CornerRadius="2">
CornerRadius="2" Cursor="Hand"
PointerPressed="MiniPreview_OnPointerPressed">
<Image Name="miniPreview" Stretch="Uniform" Margin="6" />
</Border>
<StackPanel Name="textureInfoPanel" DockPanel.Dock="Bottom" Margin="0,4,0,0" Spacing="1" />
<!-- Flat list of vehicle entries (plain text, no expanders).
Selecting one shows its mini and enables Add; double-click
replaces the selected consist vehicle. -->
<ListBox Name="vehicleListBox" Classes="Compact"
SelectionChanged="VehicleListBox_OnSelectionChanged"
DoubleTapped="VehicleListBox_OnDoubleTapped" />
DoubleTapped="VehicleListBox_OnDoubleTapped"
PointerPressed="VehicleListBox_OnPointerPressed" />
</DockPanel>
</HeaderedContentControl>
@@ -78,6 +99,11 @@
<HeaderedContentControl Grid.Column="1" Grid.Row="1" Theme="{StaticResource GroupBox}"
Header="{Binding [SelectedVehicle], Source={x:Static starterNg:App.Loc}}">
<DockPanel LastChildFill="True">
<Button Name="removeVehicleButton" DockPanel.Dock="Bottom" Classes="Flat"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Center"
Margin="0,8,0,0" IsEnabled="False" Cursor="Hand"
Click="RemoveVehicleButton_OnClick"
Content="{Binding [RemoveSelectedVehicle], Source={x:Static starterNg:App.Loc}}" />
<TabControl>
<TabItem Header="{Binding [Brakes], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
@@ -102,10 +128,26 @@
<HeaderedContentControl Grid.Row="2" Grid.ColumnSpan="2" Theme="{StaticResource GroupBox}"
Header="{Binding [Consist], Source={x:Static starterNg:App.Loc}}">
<DockPanel LastChildFill="True">
<!-- Train totals computed from the .fiz files -->
<TextBlock Name="trainStats" DockPanel.Dock="Top" FontSize="12"
Opacity="0.85" Margin="0,0,0,6" />
<Panel>
<!-- Train totals + randomize tools -->
<DockPanel DockPanel.Dock="Top" Margin="0,0,0,6">
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" Spacing="4">
<Button Name="randomTexturesButton" Classes="Basic" FontSize="11"
Padding="8,2" Cursor="Hand" Click="RandomTexturesButton_OnClick"
Content="{Binding [RandomizeTextures], Source={x:Static starterNg:App.Loc}}"
ToolTip.Tip="{Binding [TipRandomizeTextures], Source={x:Static starterNg:App.Loc}}" />
<Button Name="randomOrderButton" Classes="Basic" FontSize="11"
Padding="8,2" Cursor="Hand" Click="RandomOrderButton_OnClick"
Content="{Binding [RandomizeOrder], Source={x:Static starterNg:App.Loc}}"
ToolTip.Tip="{Binding [TipRandomizeOrder], Source={x:Static starterNg:App.Loc}}" />
<Button Name="randomOrientButton" Classes="Basic" FontSize="11"
Padding="8,2" Cursor="Hand" Click="RandomOrientButton_OnClick"
Content="{Binding [RandomizeOrientation], Source={x:Static starterNg:App.Loc}}"
ToolTip.Tip="{Binding [TipRandomizeOrientation], Source={x:Static starterNg:App.Loc}}" />
</StackPanel>
<TextBlock Name="trainStats" FontSize="12" Opacity="0.85"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
</DockPanel>
<Panel Name="consistDropTarget">
<TextBlock Name="emptyHint" VerticalAlignment="Center" HorizontalAlignment="Center"
TextWrapping="Wrap" MaxWidth="520" Opacity="0.6" TextAlignment="Center"
Text="{Binding [ConsistEmpty], Source={x:Static starterNg:App.Loc}}" />

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,46 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:starterNg="clr-namespace:StarterNG"
x:Class="StarterNG.Views.TextureBaseWindow"
x:CompileBindings="False"
Title="{Binding [TextureBaseTitle], Source={x:Static starterNg:App.Loc}}"
Width="980" Height="560"
WindowStartupLocation="CenterOwner"
MinWidth="720" MinHeight="400">
<DockPanel Margin="10">
<Grid DockPanel.Dock="Top" ColumnDefinitions="*,*,*,*" RowDefinitions="Auto,Auto,Auto"
ColumnSpacing="8" RowSpacing="6" Margin="0,0,0,8">
<CheckBox Name="fNameOn" Grid.Row="0" Grid.Column="0" Content="{Binding [TextureInfo], Source={x:Static starterNg:App.Loc}}" />
<TextBox Name="fName" Grid.Row="0" Grid.Column="1" Watermark="{Binding [Search], Source={x:Static starterNg:App.Loc}}" />
<CheckBox Name="fOpOn" Grid.Row="0" Grid.Column="2" Content="{Binding [TexOperator], Source={x:Static starterNg:App.Loc}}" />
<TextBox Name="fOp" Grid.Row="0" Grid.Column="3" />
<CheckBox Name="fMiniOn" Grid.Row="1" Grid.Column="0" Content="{Binding [Class], Source={x:Static starterNg:App.Loc}}" />
<TextBox Name="fMini" Grid.Row="1" Grid.Column="1" />
<CheckBox Name="fAuthorOn" Grid.Row="1" Grid.Column="2" Content="{Binding [TexAuthor], Source={x:Static starterNg:App.Loc}}" />
<TextBox Name="fAuthor" Grid.Row="1" Grid.Column="3" />
<CheckBox Name="fStationOn" Grid.Row="2" Grid.Column="0" Content="{Binding [TexStation], Source={x:Static starterNg:App.Loc}}" />
<TextBox Name="fStation" Grid.Row="2" Grid.Column="1" />
<CheckBox Name="fPhotoOn" Grid.Row="2" Grid.Column="2" Content="{Binding [TexPhoto], Source={x:Static starterNg:App.Loc}}" />
<TextBox Name="fPhoto" Grid.Row="2" Grid.Column="3" />
</Grid>
<TextBlock Name="statusText" DockPanel.Dock="Bottom" Margin="0,6,0,0"
FontSize="11" Opacity="0.75" />
<ListBox Name="textureList" DoubleTapped="TextureList_OnDoubleTapped">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="220,*,160,100,140" ColumnSpacing="8">
<TextBlock Grid.Column="0" Text="{Binding Skin}" TextTrimming="CharacterEllipsis" />
<TextBlock Grid.Column="1" Text="{Binding Mini}" Opacity="0.85" TextTrimming="CharacterEllipsis" />
<TextBlock Grid.Column="2" Text="{Binding Operator}" Opacity="0.85" TextTrimming="CharacterEllipsis" />
<TextBlock Grid.Column="3" Text="{Binding Revision}" Opacity="0.85" />
<TextBlock Grid.Column="4" Text="{Binding Author}" Opacity="0.75" TextTrimming="CharacterEllipsis" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</Window>

View File

@@ -0,0 +1,137 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using StarterNG.Classes;
namespace StarterNG.Views;
public partial class TextureBaseWindow : Window
{
private readonly VehicleDatabase _db = GameData.Instance.Vehicles;
private readonly List<Row> _all = new();
/// <summary>Texture chosen by double-click (null if the window was just closed).</summary>
public VehicleTexture? Picked { get; private set; }
public TextureBaseWindow()
{
InitializeComponent();
foreach (var t in _db.Textures)
{
if (_db.IsSetFollower(t)) continue;
_all.Add(new Row(t, _db));
}
foreach (var box in new Control[] { fName, fOp, fMini, fAuthor, fStation, fPhoto })
{
if (box is TextBox tb)
tb.TextChanged += (_, _) => Refresh();
}
foreach (var box in new Control[] { fNameOn, fOpOn, fMiniOn, fAuthorOn, fStationOn, fPhotoOn })
{
if (box is CheckBox cb)
cb.IsCheckedChanged += (_, _) => Refresh();
}
textureList.ContextRequested += TextureList_OnContextRequested;
Refresh();
}
private void Refresh()
{
IEnumerable<Row> q = _all;
q = Filter(q, fNameOn, fName, r => r.Skin);
q = Filter(q, fOpOn, fOp, r => r.Operator);
q = Filter(q, fMiniOn, fMini, r => r.Mini);
q = Filter(q, fAuthorOn, fAuthor, r => r.Author);
q = Filter(q, fStationOn, fStation, r => r.Station);
q = Filter(q, fPhotoOn, fPhoto, r => r.Photo);
var list = q
.OrderBy(r => r.Mini, StringComparer.OrdinalIgnoreCase)
.ThenBy(r => r.Skin, StringComparer.OrdinalIgnoreCase)
.Take(5000)
.ToList();
textureList.ItemsSource = list;
statusText.Text = $"{list.Count} / {_all.Count}";
}
private static IEnumerable<Row> Filter(
IEnumerable<Row> src, CheckBox? on, TextBox? box, Func<Row, string> field)
{
if (on?.IsChecked != true) return src;
string needle = box?.Text?.Trim() ?? "";
if (needle.Length < 1) return src;
return src.Where(r => field(r).Contains(needle, StringComparison.OrdinalIgnoreCase));
}
private void TextureList_OnDoubleTapped(object? sender, TappedEventArgs e)
{
if (textureList.SelectedItem is not Row row) return;
Picked = row.Texture;
Close();
}
private void TextureList_OnContextRequested(object? sender, ContextRequestedEventArgs e)
{
if (textureList.SelectedItem is not Row row) return;
var menu = new ContextMenu();
var copy = new MenuItem { Header = App.Loc["CopyTextureName"] };
copy.Click += async (_, _) =>
{
if (Clipboard != null)
await Clipboard.SetTextAsync(row.Skin);
};
menu.Items.Add(copy);
var open = new MenuItem { Header = App.Loc["OpenTextureFolder"] };
open.Click += (_, _) =>
{
string dir = Path.Combine(Directory.GetCurrentDirectory(), "dynamic",
row.Texture.Directory.Replace('/', Path.DirectorySeparatorChar).TrimEnd('\\', '/'));
if (!Directory.Exists(dir)) return;
try
{
Process.Start(new ProcessStartInfo(dir) { UseShellExecute = true });
}
catch { }
};
menu.Items.Add(open);
menu.Open(textureList);
e.Handled = true;
}
private sealed class Row
{
public VehicleTexture Texture { get; }
public string Skin { get; }
public string Mini { get; }
public string Operator { get; }
public string Revision { get; }
public string Author { get; }
public string Station { get; }
public string Photo { get; }
public Row(VehicleTexture t, VehicleDatabase db)
{
Texture = t;
Skin = Path.GetFileNameWithoutExtension(t.Skinfile);
Mini = db.ResolveMiniName(t) ?? t.ResolvedClass ?? "";
Operator = t.Meta?.Operator ?? "";
Revision = t.Meta?.RevisionDate ?? "";
Author = t.Meta?.TextureAuthor ?? "";
Station = t.Meta?.Depot ?? "";
Photo = t.Meta?.PhotoAuthor ?? "";
}
}
}