Layout improvements, depot behavior improvements

This commit is contained in:
2026-06-23 23:24:46 +02:00
parent 1bfac8b222
commit d932761786
11 changed files with 422 additions and 203 deletions

View File

@@ -279,10 +279,24 @@ public class VehicleDatabase
if (t.Group != null) if (t.Group != null)
GroupsById.TryGetValue(t.Group, out g); GroupsById.TryGetValue(t.Group, out g);
t.ResolvedCategory = g?.Category; string mini = g != null && !string.IsNullOrEmpty(g.Mini)
t.ResolvedClass = g != null && !string.IsNullOrEmpty(g.Mini)
? g.Mini! ? g.Mini!
: t.MiniRef ?? ""; : t.MiniRef ?? "";
string? category = g?.Category;
// A "*" category is a wildcard: the actual category is the first letter
// of the mini (upper-cased, like the original Starter's UpCase(mini[1])
// wagon-typing), e.g. a "Bdhpumn" mini -> category "B".
if (category == "*")
{
string source = !string.IsNullOrEmpty(mini) ? mini : (t.TextureMini ?? "");
if (source.Length > 0)
category = char.ToUpperInvariant(source[0]).ToString();
}
t.ResolvedCategory = category;
t.ResolvedClass = mini;
} }
} }

View File

@@ -27,10 +27,15 @@
BorderThickness="0,0,0,1"> BorderThickness="0,0,0,1">
<Grid RowDefinitions="32,48"> <Grid RowDefinitions="32,48">
<!-- Row 0: title / caption strip (draggable; OS buttons top-right) --> <!-- Row 0: title / caption strip. Transparent background makes the
<TextBlock Grid.Row="0" Text="Starter MaSzyna" VerticalAlignment="Center" whole strip a drag handle (PointerPressed -> BeginMoveDrag),
Margin="14,0,0,0" FontSize="11" and the OS buttons render at its top-right. -->
Foreground="{StaticResource CarbonFgDim}" /> <Border Grid.Row="0" Background="Transparent"
PointerPressed="TitleBar_OnPointerPressed">
<TextBlock Text="Starter MaSzyna" VerticalAlignment="Center"
Margin="14,0,0,0" FontSize="11"
Foreground="{StaticResource CarbonFgDim}" />
</Border>
<!-- Row 1: logo + navigation (left) and the right cluster (right) --> <!-- Row 1: logo + navigation (left) and the right cluster (right) -->
<Grid Grid.Row="1" ColumnDefinitions="Auto,Auto,*,Auto"> <Grid Grid.Row="1" ColumnDefinitions="Auto,Auto,*,Auto">

View File

@@ -5,6 +5,7 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Threading; using Avalonia.Threading;
using StarterNG.Classes; using StarterNG.Classes;
@@ -32,6 +33,13 @@ public partial class MainWindow : Window
LangCombo.SelectedIndex = App.Loc.CurrentLanguage == "Polski" ? 0 : 1; LangCombo.SelectedIndex = App.Loc.CurrentLanguage == "Polski" ? 0 : 1;
} }
// Lets the user drag the window by the top caption strip.
private void TitleBar_OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
BeginMoveDrag(e);
}
// ── Navigation: show the page whose RadioButton was just checked ────────── // ── Navigation: show the page whose RadioButton was just checked ──────────
private void Nav_OnCheckedChanged(object? sender, RoutedEventArgs e) private void Nav_OnCheckedChanged(object? sender, RoutedEventArgs e)
{ {

View File

@@ -8,7 +8,7 @@
<BuiltInComInteropSupport>true</BuiltInComInteropSupport> <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault> <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<AssemblyVersion>1.0.0.1 </AssemblyVersion> <AssemblyVersion>1.0.0.1</AssemblyVersion>
<FileVersion>1.0.0.1</FileVersion> <FileVersion>1.0.0.1</FileVersion>
<AssemblyName>Starter</AssemblyName> <AssemblyName>Starter</AssemblyName>
<Company>eu07.pl</Company> <Company>eu07.pl</Company>

View File

@@ -171,11 +171,13 @@
<Setter Property="Background" Value="{StaticResource CarbonAccent}" /> <Setter Property="Background" Value="{StaticResource CarbonAccent}" />
</Style> </Style>
<!-- ── Tabs: smaller headers (Fluent's default is oversized) ──────────── --> <!-- ── Tabs: smaller headers than Fluent's default, but tall enough that the
green selection pipe at the bottom doesn't touch the label. ─────────── -->
<Style Selector="TabItem"> <Style Selector="TabItem">
<Setter Property="FontSize" Value="14" /> <Setter Property="FontSize" Value="14" />
<Setter Property="MinHeight" Value="0" /> <Setter Property="MinHeight" Value="38" />
<Setter Property="Padding" Value="10,6" /> <Setter Property="Padding" Value="12,4,12,8" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style> </Style>
<!-- ── Compact list (tight rows that fill the available space) ─────────── --> <!-- ── Compact list (tight rows that fill the available space) ─────────── -->
@@ -187,4 +189,14 @@
<Setter Property="MinHeight" Value="0" /> <Setter Property="MinHeight" Value="0" />
<Setter Property="FontSize" Value="12" /> <Setter Property="FontSize" Value="12" />
</Style> </Style>
<!-- ── Compact tree (tight scenery rows) ──────────────────────────────── -->
<Style Selector="TreeView.Compact TreeViewItem">
<Setter Property="MinHeight" Value="22" />
<Setter Property="FontSize" Value="12" />
</Style>
<Style Selector="TreeView.Compact TreeViewItem /template/ Border#PART_LayoutRoot">
<Setter Property="MinHeight" Value="22" />
<Setter Property="Padding" Value="4,1" />
</Style>
</Styles> </Styles>

View File

@@ -13,9 +13,11 @@
<ColumnDefinition /> <ColumnDefinition />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- Consists list and the brakes/loads editor share the upper area
<RowDefinition /> evenly, so the editor has room for all its controls. -->
<RowDefinition Height="220" /> <RowDefinition Height="*" />
<RowDefinition Height="*" MinHeight="210" />
<RowDefinition Height="190" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<!-- Vehicle database browser (left, rows 0-1) --> <!-- Vehicle database browser (left, rows 0-1) -->
@@ -57,43 +59,31 @@
</DockPanel> </DockPanel>
</HeaderedContentControl> </HeaderedContentControl>
<!-- Scenery + load consist (top-right) --> <!-- Consists present on the scenery selected in the Scenarios tab.
Selecting one loads it into the editor below. -->
<HeaderedContentControl Grid.Column="1" Grid.Row="0" Theme="{StaticResource GroupBox}" <HeaderedContentControl Grid.Column="1" Grid.Row="0" Theme="{StaticResource GroupBox}"
Header="{Binding [LoadFromScenery], Source={x:Static starterNg:App.Loc}}"> Header="{Binding [MapConsists], Source={x:Static starterNg:App.Loc}}">
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto" ColumnSpacing="8" RowSpacing="6"> <DockPanel LastChildFill="True">
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" <TextBlock Name="mapSceneryLabel" DockPanel.Dock="Top" FontSize="12"
Text="{Binding [Scenery], Source={x:Static starterNg:App.Loc}}" /> Opacity="0.8" Margin="0,0,0,6" TextTrimming="CharacterEllipsis" />
<ComboBox Name="sceneryCombo" Grid.Row="0" Grid.Column="1" HorizontalAlignment="Stretch" <ListBox Name="sceneryConsistList" Classes="Compact"
SelectionChanged="SceneryCombo_OnSelectionChanged" /> SelectionChanged="SceneryConsistList_OnSelectionChanged" />
<ComboBox Name="sceneryConsistCombo" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" </DockPanel>
HorizontalAlignment="Stretch" MaxDropDownHeight="420"
PlaceholderText="{Binding [LoadFromScenery], Source={x:Static starterNg:App.Loc}}"
DropDownOpened="SceneryConsistCombo_OnDropDownOpened"
SelectionChanged="SceneryConsistCombo_OnSelectionChanged" />
</Grid>
</HeaderedContentControl> </HeaderedContentControl>
<!-- Brakes / Loads editor (middle-right) --> <!-- Brakes / Loads editor (middle-right) -->
<HeaderedContentControl Grid.Column="1" Grid.Row="1" Theme="{StaticResource GroupBox}" <HeaderedContentControl Grid.Column="1" Grid.Row="1" Theme="{StaticResource GroupBox}"
Header="{Binding [SelectedVehicle], Source={x:Static starterNg:App.Loc}}"> Header="{Binding [SelectedVehicle], Source={x:Static starterNg:App.Loc}}">
<DockPanel LastChildFill="True"> <DockPanel LastChildFill="True">
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8" Margin="0,0,0,8">
<Button Name="newConsistButton" Classes="Flat" Cursor="Hand"
Click="NewConsist_OnClick"
Content="{Binding [NewConsist], Source={x:Static starterNg:App.Loc}}" />
<TextBlock Name="summaryLabel" VerticalAlignment="Center" Opacity="0.8" FontSize="12" />
</StackPanel>
<TabControl> <TabControl>
<TabItem Header="{Binding [Brakes], Source={x:Static starterNg:App.Loc}}"> <TabItem Header="{Binding [Brakes], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto"> <ScrollViewer VerticalScrollBarVisibility="Auto">
<TextBlock Name="brakesContent" TextWrapping="Wrap" FontSize="12" <StackPanel Name="brakesPanel" Margin="4" Spacing="8" />
LineHeight="18" Margin="4" />
</ScrollViewer> </ScrollViewer>
</TabItem> </TabItem>
<TabItem Header="{Binding [Loads], Source={x:Static starterNg:App.Loc}}"> <TabItem Header="{Binding [Loads], Source={x:Static starterNg:App.Loc}}">
<ScrollViewer VerticalScrollBarVisibility="Auto"> <ScrollViewer VerticalScrollBarVisibility="Auto">
<TextBlock Name="loadsContent" TextWrapping="Wrap" FontSize="12" <StackPanel Name="loadsPanel" Margin="4" Spacing="8" />
LineHeight="18" Margin="4" />
</ScrollViewer> </ScrollViewer>
</TabItem> </TabItem>
</TabControl> </TabControl>

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
@@ -40,8 +41,8 @@ public partial class Depot : UserControl
// debounces search input so we don't rebuild on every keystroke // debounces search input so we don't rebuild on every keystroke
private DispatcherTimer? _searchTimer; private DispatcherTimer? _searchTimer;
// scenery-consist previews are built lazily on first dropdown open // the scenery whose consists are listed on the right (from the Scenarios tab)
private bool _consistPreviewsBuilt; private Scenery? _listScenery;
// active database filters // active database filters
private Func<string?, bool>? _categoryFilter; // matches a category letter, null = none chosen private Func<string?, bool>? _categoryFilter; // matches a category letter, null = none chosen
@@ -141,16 +142,26 @@ public partial class Depot : UserControl
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
PopulateCategoryCombo(); // -> populates class combo -> builds browser PopulateCategoryCombo(); // -> populates class combo -> builds browser
PopulateSceneryCombo(); // -> populates consist combo for the first scenery PopulateSceneryConsists(); // -> lists the current scenery's consists
RebuildConsist(); RebuildConsist();
}, DispatcherPriority.Background); }, DispatcherPriority.Background);
// When the tab is shown, pick up the consist selected in the scenery view. // Switching tabs only toggles IsVisible (the view stays in the visual
// tree), so refresh whenever the depot becomes visible - this picks up a
// scenery changed in the Scenarios tab in the meantime.
AttachedToVisualTree += (_, _) => SyncFromSelection(); AttachedToVisualTree += (_, _) => SyncFromSelection();
PropertyChanged += (_, e) =>
{
if (e.Property == IsVisibleProperty && IsVisible)
SyncFromSelection();
};
} }
private void SyncFromSelection() private void SyncFromSelection()
{ {
// refresh the on-map consists list to the scenery picked in the Scenarios tab
PopulateSceneryConsists();
var trainset = AppState.Instance.CurrentTrainset; var trainset = AppState.Instance.CurrentTrainset;
if (trainset != null && !ReferenceEquals(trainset, _editingTrainset)) if (trainset != null && !ReferenceEquals(trainset, _editingTrainset))
LoadTrainset(trainset); LoadTrainset(trainset);
@@ -497,116 +508,62 @@ public partial class Depot : UserControl
// ------------------------------------------------------------- scenery combos // ------------------------------------------------------------- scenery combos
private void PopulateSceneryCombo() // Lists the consists of the scenery currently selected in the Scenarios tab,
// so they can be picked and edited here.
private void PopulateSceneryConsists()
{ {
_suppress = true; _suppress = true;
sceneryCombo.Items.Clear(); sceneryConsistList.Items.Clear();
foreach (var scenery in _sceneries)
_listScenery = AppState.Instance.CurrentScenery ?? _sceneries.FirstOrDefault();
mapSceneryLabel.Text = _listScenery != null
? Path.GetFileNameWithoutExtension(_listScenery.Path)
: App.Loc["NoSceneryConsists"];
if (_listScenery != null)
{ {
sceneryCombo.Items.Add(new ComboBoxItem foreach (var trainset in _listScenery.Trainsets)
{
Content = Path.GetFileNameWithoutExtension(scenery.Path),
Tag = scenery
});
}
if (sceneryCombo.Items.Count > 0)
sceneryCombo.SelectedIndex = 0;
_suppress = false;
PopulateConsistCombo(_sceneries.FirstOrDefault());
}
private void PopulateConsistCombo(Scenery? scenery)
{
_suppress = true;
sceneryConsistCombo.Items.Clear();
if (scenery != null)
{
foreach (var trainset in scenery.Trainsets)
{ {
if (trainset.Vehicles.Count == 0) if (trainset.Vehicles.Count == 0)
continue; continue;
if (trainset.Vehicles.All(v => string.IsNullOrWhiteSpace(v.Name))) if (trainset.Vehicles.All(v => string.IsNullOrWhiteSpace(v.Name)))
continue; continue;
// lightweight text content now; the rich mini-preview is built var entry = new ListBoxItem
// lazily the first time the dropdown is opened
sceneryConsistCombo.Items.Add(new ComboBoxItem
{ {
Content = ConsistSummary(trainset), Content = ConsistText(trainset),
Tag = trainset Tag = trainset
}); };
sceneryConsistList.Items.Add(entry);
if (ReferenceEquals(trainset, AppState.Instance.CurrentTrainset))
sceneryConsistList.SelectedItem = entry;
} }
} }
_consistPreviewsBuilt = false;
sceneryConsistCombo.SelectedItem = null;
_suppress = false; _suppress = false;
} }
private static string ConsistSummary(Trainset trainset) // Selecting a consist on the map loads it into the editor below.
{ private void SceneryConsistList_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
string text = string.Join(" + ", trainset.Vehicles.Select(v => v.SkinFile));
return text.Length > 70 ? text[..67] + "..." : text;
}
// Builds the mini-thumbnail previews only once, on first dropdown open.
private void SceneryConsistCombo_OnDropDownOpened(object? sender, EventArgs e)
{
if (_consistPreviewsBuilt) return;
_consistPreviewsBuilt = true;
foreach (var obj in sceneryConsistCombo.Items)
if (obj is ComboBoxItem { Tag: Trainset trainset } item)
item.Content = BuildConsistPreview(trainset);
}
// Combo item: row of mini thumbnails on top, skin names underneath.
private Control BuildConsistPreview(Trainset trainset)
{
var minis = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 2 };
var names = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 6 };
foreach (var v in trainset.Vehicles)
{
var bmp = GetMiniBitmap(_db.MiniForSkin(v.SkinFile) ?? v.MiniName, 20);
if (bmp != null)
minis.Children.Add(new Image { Source = bmp, Height = 20, Stretch = Stretch.Uniform });
else
minis.Children.Add(new Border
{
Height = 20, Width = 32, Background = Placeholder, CornerRadius = new CornerRadius(3)
});
names.Children.Add(new TextBlock { Text = v.SkinFile, FontSize = 10, Opacity = 0.8 });
}
var panel = new StackPanel { Spacing = 3 };
panel.Children.Add(minis);
panel.Children.Add(names);
return panel;
}
private void SceneryCombo_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{ {
if (_suppress) return; if (_suppress) return;
var scenery = (sceneryCombo.SelectedItem as ComboBoxItem)?.Tag as Scenery; if (sceneryConsistList.SelectedItem is not ListBoxItem { Tag: Trainset trainset })
PopulateConsistCombo(scenery);
}
// Selecting a scenery consist binds the editor to that trainset.
private void SceneryConsistCombo_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (_suppress) return;
if (sceneryConsistCombo.SelectedItem is not ComboBoxItem { Tag: Trainset trainset })
return; return;
AppState.Instance.CurrentScenery = (sceneryCombo.SelectedItem as ComboBoxItem)?.Tag as Scenery; AppState.Instance.CurrentScenery = _listScenery;
AppState.Instance.CurrentTrainset = trainset; AppState.Instance.CurrentTrainset = trainset;
LoadTrainset(trainset); LoadTrainset(trainset);
} }
// Plain-text label for a scenery consist (no thumbnails).
private static string ConsistText(Trainset trainset)
{
string text = string.Join(" + ", trainset.Vehicles.Select(v => v.SkinFile));
if (string.IsNullOrWhiteSpace(text))
text = string.Join(" + ", trainset.Vehicles.Select(v => v.Name));
return text.Length > 80 ? text[..77] + "…" : text;
}
// ------------------------------------------------------------ consist edits // ------------------------------------------------------------ consist edits
private Dynamic MakeDynamic(VehicleTexture texture) private Dynamic MakeDynamic(VehicleTexture texture)
@@ -728,7 +685,6 @@ public partial class Depot : UserControl
} }
emptyHint.IsVisible = _consist.Count == 0; emptyHint.IsVisible = _consist.Count == 0;
UpdateSummary();
UpdateDetails(); UpdateDetails();
WriteBackToScenery(); WriteBackToScenery();
} }
@@ -849,22 +805,123 @@ public partial class Depot : UserControl
Cursor = _hand, Cursor = _hand,
Child = inner Child = inner
}; };
card.PointerPressed += (_, _) => card.PointerPressed += (_, e) =>
{ {
// left button only - right-click is reserved for the context popup,
// and rebuilding here would detach the card it anchors to
if (!e.GetCurrentPoint(card).Properties.IsLeftButtonPressed)
return;
_selected = item; _selected = item;
RebuildConsist(); RebuildConsist();
// mirror the click into the database browser: pick the category and
// highlight this vehicle's entry there
SelectInBrowser(item.Cars[0]);
};
// right-click a vehicle to set its wagon number (node "#<n>" suffix)
card.ContextRequested += (_, args) =>
{
ShowWagonNumberFlyout(card, item.Cars[0]);
args.Handled = true;
}; };
return card; return card;
} }
// Selects, in the left database browser, the category and the list entry of the
// given vehicle - so clicking a consist car reveals it in the database.
private void SelectInBrowser(Dynamic car)
{
var texture = _db.TextureForSkin(car.SkinFile);
if (texture is null)
return;
string? category = CategoryOf(texture);
var catItem = categoryCombo.Items.OfType<ComboBoxItem>()
.FirstOrDefault(it => it.Tag is Func<string?, bool> f && f(category));
if (catItem != null && !ReferenceEquals(categoryCombo.SelectedItem, catItem))
{
categoryCombo.SelectedItem = catItem; // -> rebuilds class combo (class=all) + list
}
else
{
// same category already chosen: drop any class filter so the entry shows
_suppress = true;
classCombo.SelectedIndex = -1;
_suppress = false;
_classFilter = null;
BuildList();
}
// highlight the matching entry (selection drives the mini + Add button)
foreach (var obj in vehicleListBox.Items)
if (obj is ListBoxItem { Tag: VehicleTexture t } entry &&
string.Equals(t.Skinfile, texture.Skinfile, StringComparison.OrdinalIgnoreCase) &&
string.Equals(t.Directory, texture.Directory, StringComparison.OrdinalIgnoreCase))
{
vehicleListBox.SelectedItem = entry;
entry.BringIntoView();
break;
}
}
// The "#<n>" number written into the node name (e.g. EU07-005#11). 0 = none.
private static int GetWagonNumber(Dynamic car)
{
string name = car.Name ?? "";
int hash = name.LastIndexOf('#');
if (hash >= 0 && hash + 1 < name.Length && name[(hash + 1)..].All(char.IsDigit)
&& int.TryParse(name[(hash + 1)..], out int n))
return n;
return 0;
}
private void SetWagonNumber(Dynamic car, int number)
{
string name = car.Name ?? "";
int hash = name.LastIndexOf('#');
string baseName = (hash >= 0 && hash + 1 < name.Length && name[(hash + 1)..].All(char.IsDigit))
? name[..hash]
: name;
car.Name = number > 0 ? $"{baseName}#{number}" : baseName;
RebuildConsist();
}
// Small right-click popup to type a wagon number for the vehicle.
private void ShowWagonNumberFlyout(Control anchor, Dynamic car)
{
var num = new NumericUpDown
{
Minimum = 0, Maximum = 99999, Increment = 1, FormatString = "0",
Value = GetWagonNumber(car), MinWidth = 120
};
var apply = new Button { Content = App.Loc["Set"], HorizontalAlignment = HorizontalAlignment.Right };
apply.Classes.Add("Flat");
apply.Classes.Add("Accent");
var panel = new StackPanel { Spacing = 6, Margin = new Thickness(8), MinWidth = 160 };
panel.Children.Add(new TextBlock { Text = App.Loc["WagonNumber"], FontWeight = FontWeight.Bold, FontSize = 12 });
panel.Children.Add(num);
panel.Children.Add(apply);
var flyout = new Flyout { Content = panel, Placement = PlacementMode.Top };
apply.Click += (_, _) =>
{
SetWagonNumber(car, (int)(num.Value ?? 0));
flyout.Hide();
};
flyout.ShowAt(anchor);
}
private static string UnitLabel(ConsistItem item) private static string UnitLabel(ConsistItem item)
{ {
string key = UnitKey(item.Cars[0]); string key = UnitKey(item.Cars[0]);
return item.Cars.Count > 1 ? $"{key} [{item.Cars.Count}]" : key; return item.Cars.Count > 1 ? $"{key} [{item.Cars.Count}]" : key;
} }
// Coupling indicator between two units. Hovering shows the 8-bit coupling // Coupling indicator between two units. Click it to open a (clickable) flyout
// editor for the left unit's last car (placeholder; full editing comes later). // with the 8-bit coupling editor for the left unit's last car - a flyout, not a
// tooltip, so it stays open while you toggle the options.
private Control BuildCoupler(ConsistItem item) private Control BuildCoupler(ConsistItem item)
{ {
var glyph = new TextBlock var glyph = new TextBlock
@@ -875,17 +932,21 @@ public partial class Depot : UserControl
VerticalAlignment = VerticalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center HorizontalAlignment = HorizontalAlignment.Center
}; };
var coupler = new Border var coupler = new Button
{ {
Width = 18,
Padding = new Thickness(2), Padding = new Thickness(2),
MinWidth = 0,
Cursor = _hand, Cursor = _hand,
Background = Brushes.Transparent,
VerticalAlignment = VerticalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
Child = glyph Content = glyph,
Flyout = new Flyout
{
Content = BuildCouplingBox(item.Cars[^1]),
Placement = PlacementMode.Top
}
}; };
ToolTip.SetTip(coupler, BuildCouplingBox(item.Cars[^1])); coupler.Classes.Add("Basic");
ToolTip.SetShowDelay(coupler, 150); ToolTip.SetTip(coupler, App.Loc["Coupling"]);
return coupler; return coupler;
} }
@@ -914,40 +975,7 @@ public partial class Depot : UserControl
panel.Children.Add(check); panel.Children.Add(check);
} }
// Brake-rack setting (the "B" parameter of the coupling field). // The brake setting lives in the Brakes tab now, not here.
panel.Children.Add(new TextBlock
{
Text = "Brake mode",
FontWeight = FontWeight.Bold,
FontSize = 11,
Margin = new Thickness(0, 6, 0, 2)
});
var brakeCombo = new ComboBox { FontSize = 11, MinWidth = 90 };
brakeCombo.Items.Add(new ComboBoxItem { Content = "(none)", Tag = null });
foreach (string mode in BrakeSetting.Modes)
brakeCombo.Items.Add(new ComboBoxItem { Content = mode, Tag = mode });
string? current = d.Coupling.GetBrake()?.Mode;
brakeCombo.SelectedIndex = current is null
? 0
: Array.IndexOf(BrakeSetting.Modes, current) + 1;
brakeCombo.SelectionChanged += (_, _) =>
{
if ((brakeCombo.SelectedItem as ComboBoxItem)?.Tag is not string mode)
{
d.Coupling.SetBrake(null);
}
else
{
var brake = d.Coupling.GetBrake() ?? new BrakeSetting();
brake.Mode = mode;
d.Coupling.SetBrake(brake);
}
};
panel.Children.Add(brakeCombo);
return new Border { Padding = new Thickness(8), Child = panel }; return new Border { Padding = new Thickness(8), Child = panel };
} }
@@ -983,32 +1011,177 @@ public partial class Depot : UserControl
return btn; return btn;
} }
private void UpdateSummary() // Brakes / Loads editors for the selected unit (applied to all of its cars).
{
int cars = _consist.Sum(i => i.Cars.Count);
int crewed = _consist.Count(i => i.Driver is eDriverType.Headdriver or eDriverType.Reardriver);
summaryLabel.Text = $"{cars} {App.Loc["Vehicles"]} · {crewed} {App.Loc["Crew"]}";
}
// Brakes / Loads tabs are placeholders bound to the selected unit.
private void UpdateDetails() private void UpdateDetails()
{ {
brakesPanel.Children.Clear();
loadsPanel.Children.Clear();
if (_selected is null) if (_selected is null)
{ {
brakesContent.Text = App.Loc["SelectVehicleHint"]; brakesPanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
loadsContent.Text = App.Loc["SelectVehicleHint"]; loadsPanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
return; return;
} }
var lead = _selected.Cars[0]; BuildBrakesEditor(_selected);
string title = _selected.Grouped ? UnitLabel(_selected) : lead.Name; BuildLoadsEditor(_selected);
brakesContent.Text = }
$"{App.Loc["Vehicle"]}: {title}\n" +
$"cars: {string.Join(", ", _selected.Cars.Select(c => c.SkinFile))}\n\n" + private static TextBlock DetailHint(string text) => new()
$"{App.Loc["ComingSoon"]}"; {
loadsContent.Text = Text = text, Opacity = 0.6, FontSize = 12, TextWrapping = TextWrapping.Wrap
$"{App.Loc["Vehicle"]}: {title}\n\n" + };
$"{App.Loc["ComingSoon"]}";
private static TextBlock UnitTitle(ConsistItem item) => new()
{
Text = item.Grouped ? UnitLabel(item) : item.Cars[0].Name,
FontWeight = FontWeight.Bold, FontSize = 12
};
// Row of "<label> <control>".
private static Control LabeledRow(string label, Control control)
{
var sp = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 };
sp.Children.Add(new TextBlock
{
Text = label, VerticalAlignment = VerticalAlignment.Center,
FontSize = 12, MinWidth = 120
});
sp.Children.Add(control);
return sp;
}
private static void AddOption(ComboBox combo, string content, string? tag)
=> combo.Items.Add(new ComboBoxItem { Content = content, Tag = tag });
// Brake mode / load adaptation / switch, written into each car's coupling "B" param.
private void BuildBrakesEditor(ConsistItem item)
{
var brake = item.Cars[0].Coupling.GetBrake();
var modeCombo = new ComboBox { FontSize = 12, MinWidth = 150 };
AddOption(modeCombo, App.Loc["None"], null);
foreach (var m in BrakeSetting.Modes) AddOption(modeCombo, m, m);
modeCombo.SelectedIndex = brake?.Mode is { } mode
? Array.IndexOf(BrakeSetting.Modes, mode) + 1 : 0;
string?[] loads = { null, "T", "H", "F", "A" };
var loadCombo = new ComboBox { FontSize = 12, MinWidth = 150 };
foreach (var l in loads) AddOption(loadCombo, l ?? App.Loc["None"], l);
loadCombo.SelectedIndex = Math.Max(0, Array.IndexOf(loads, brake?.Load));
string?[] switches = { null, "0", "1", "A" };
var switchCombo = new ComboBox { FontSize = 12, MinWidth = 150 };
foreach (var s in switches) AddOption(switchCombo, s ?? App.Loc["None"], s);
switchCombo.SelectedIndex = Math.Max(0, Array.IndexOf(switches, brake?.Switch));
void Apply()
{
string? selMode = (modeCombo.SelectedItem as ComboBoxItem)?.Tag as string;
if (selMode is null)
{
foreach (var c in item.Cars) c.Coupling.SetBrake(null);
return;
}
var b = new BrakeSetting
{
Mode = selMode,
Load = (loadCombo.SelectedItem as ComboBoxItem)?.Tag as string,
Switch = (switchCombo.SelectedItem as ComboBoxItem)?.Tag as string
};
foreach (var c in item.Cars) c.Coupling.SetBrake(b);
}
modeCombo.SelectionChanged += (_, _) => Apply();
loadCombo.SelectionChanged += (_, _) => Apply();
switchCombo.SelectionChanged += (_, _) => Apply();
brakesPanel.Children.Add(UnitTitle(item));
brakesPanel.Children.Add(LabeledRow(App.Loc["BrakeMode"], modeCombo));
brakesPanel.Children.Add(LabeledRow(App.Loc["BrakeLoad"], loadCombo));
brakesPanel.Children.Add(LabeledRow(App.Loc["BrakeSwitch"], switchCombo));
}
// Cargo type + amount, written into each car's node::dynamic trailing params.
private void BuildLoadsEditor(ConsistItem item)
{
var lead = item.Cars[0];
// Suggest cargo names from data/load_weights.txt (same source the original
// Starter uses), while still allowing a free-typed value.
var typeBox = new AutoCompleteBox
{
FontSize = 12,
MinWidth = 200,
Text = lead.LoadType ?? "",
ItemsSource = LoadTypes(),
FilterMode = AutoCompleteFilterMode.ContainsOrdinal
};
var countBox = new NumericUpDown
{
FontSize = 12, Minimum = 0, Maximum = 1000, Increment = 1,
Value = lead.LoadCount, MinWidth = 120, FormatString = "0"
};
void Apply()
{
string type = typeBox.Text?.Trim() ?? "";
int count = (int)(countBox.Value ?? 0);
foreach (var c in item.Cars)
{
c.LoadCount = count;
c.LoadType = count > 0 && !string.IsNullOrEmpty(type) ? type : c.LoadType;
if (count > 0)
c.HasVelocity = true; // velocity token must precede loadcount
}
}
typeBox.LostFocus += (_, _) => Apply();
countBox.ValueChanged += (_, _) => Apply();
loadsPanel.Children.Add(UnitTitle(item));
loadsPanel.Children.Add(LabeledRow(App.Loc["LoadType"], typeBox));
loadsPanel.Children.Add(LabeledRow(App.Loc["LoadCount"], countBox));
loadsPanel.Children.Add(new TextBlock
{
Text = App.Loc["LoadHint"], Opacity = 0.6, FontSize = 11, TextWrapping = TextWrapping.Wrap
});
}
// Cargo names parsed once from data/load_weights.txt (the file the simulator
// and the original Starter use), for the load-type suggestions. Each line is
// "<name>: <weight>"; only the name (before the colon) is used here.
private static List<string>? _loadTypes;
private static IReadOnlyList<string> LoadTypes()
{
if (_loadTypes != null)
return _loadTypes;
var list = new List<string>();
try
{
string path = Path.Combine("data", "load_weights.txt");
if (File.Exists(path))
{
foreach (var raw in File.ReadAllLines(path, Encoding.GetEncoding(1250)))
{
string line = raw.Trim();
if (line.Length == 0 || line.StartsWith("#") || line.StartsWith("//"))
continue;
int colon = line.IndexOf(':');
string name = (colon >= 0 ? line[..colon] : line).Trim();
if (name.Length > 0)
list.Add(name);
}
}
}
catch { /* missing / unreadable - just no suggestions */ }
_loadTypes = list.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(s => s, StringComparer.OrdinalIgnoreCase)
.ToList();
return _loadTypes;
} }
// ------------------------------------------------------------------- minis // ------------------------------------------------------------------- minis
@@ -1049,18 +1222,6 @@ public partial class Depot : UserControl
_searchTimer.Start(); _searchTimer.Start();
} }
private void NewConsist_OnClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
// detach from any scenery trainset - this consist is now standalone
_editingTrainset = null;
AppState.Instance.CurrentTrainset = null;
_consist.Clear();
_selected = null;
_suppress = true;
sceneryConsistCombo.SelectedItem = null;
_suppress = false;
RebuildConsist();
}
} }
// One entry in the consist: a single vehicle or a locked multi-car unit. // One entry in the consist: a single vehicle or a locked multi-car unit.

View File

@@ -21,7 +21,8 @@
<HeaderedContentControl Grid.Column="0" Grid.Row="0" Theme="{StaticResource GroupBox}" <HeaderedContentControl Grid.Column="0" Grid.Row="0" Theme="{StaticResource GroupBox}"
Classes="Compact" Classes="Compact"
Header="{Binding [Sceneries], Source={x:Static starterNg:App.Loc}}"> Header="{Binding [Sceneries], Source={x:Static starterNg:App.Loc}}">
<TreeView Name="sceneryList" SelectionChanged="SceneryList_OnSelectionChanged" /> <TreeView Name="sceneryList" Classes="Compact"
SelectionChanged="SceneryList_OnSelectionChanged" />
</HeaderedContentControl> </HeaderedContentControl>
<!-- Centre: trainsets list (compact, fills) + description/weather/timetable tabs --> <!-- Centre: trainsets list (compact, fills) + description/weather/timetable tabs -->

View File

@@ -68,8 +68,14 @@ public partial class Scenarios : UserControl
} }
// refresh the consist preview when the view is shown again (e.g. after // refresh the consist preview when the view is shown again (e.g. after
// editing the consist in the depot) // editing the consist in the depot). Switching tabs only toggles IsVisible
// (the view stays in the tree), so listen for that too.
AttachedToVisualTree += (_, _) => RefreshSelectedConsist(); AttachedToVisualTree += (_, _) => RefreshSelectedConsist();
PropertyChanged += (_, e) =>
{
if (e.Property == IsVisibleProperty && IsVisible)
RefreshSelectedConsist();
};
} }
private void SceneryList_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) private void SceneryList_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)

View File

@@ -115,6 +115,17 @@
<String key="Brakes">Brakes</String> <String key="Brakes">Brakes</String>
<String key="Loads">Loads</String> <String key="Loads">Loads</String>
<String key="Coupling">Coupling</String> <String key="Coupling">Coupling</String>
<String key="MapConsists">Consists on the scenery</String>
<String key="NoSceneryConsists">Select a scenery in the Scenarios tab.</String>
<String key="BrakeMode">Brake mode</String>
<String key="BrakeLoad">Load setting</String>
<String key="BrakeSwitch">Brake switch</String>
<String key="None"></String>
<String key="LoadType">Load type</String>
<String key="LoadCount">Amount</String>
<String key="LoadHint">Set an amount above 0 to load the vehicle.</String>
<String key="WagonNumber">Wagon number</String>
<String key="Set">Set</String>
<String key="SelectVehicleHint">Select a vehicle in the consist to edit it.</String> <String key="SelectVehicleHint">Select a vehicle in the consist to edit it.</String>
<String key="ComingSoon">Editing options coming soon.</String> <String key="ComingSoon">Editing options coming soon.</String>
<String key="Split">Split</String> <String key="Split">Split</String>

View File

@@ -115,6 +115,17 @@
<String key="Brakes">Hamulce</String> <String key="Brakes">Hamulce</String>
<String key="Loads">Ładunki</String> <String key="Loads">Ładunki</String>
<String key="Coupling">Sprzęg</String> <String key="Coupling">Sprzęg</String>
<String key="MapConsists">Składy na scenerii</String>
<String key="NoSceneryConsists">Wybierz scenerię w zakładce Scenariusze.</String>
<String key="BrakeMode">Tryb hamulca</String>
<String key="BrakeLoad">Nastawa ładunkowa</String>
<String key="BrakeSwitch">Włącznik hamulca</String>
<String key="None"></String>
<String key="LoadType">Rodzaj ładunku</String>
<String key="LoadCount">Ilość</String>
<String key="LoadHint">Ustaw ilość większą od 0, aby załadować pojazd.</String>
<String key="WagonNumber">Numer wagonu</String>
<String key="Set">Ustaw</String>
<String key="SelectVehicleHint">Wybierz pojazd w składzie, aby go edytować.</String> <String key="SelectVehicleHint">Wybierz pojazd w składzie, aby go edytować.</String>
<String key="ComingSoon">Opcje edycji wkrótce.</String> <String key="ComingSoon">Opcje edycji wkrótce.</String>
<String key="Split">Rozdziel</String> <String key="Split">Rozdziel</String>