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)
GroupsById.TryGetValue(t.Group, out g);
t.ResolvedCategory = g?.Category;
t.ResolvedClass = g != null && !string.IsNullOrEmpty(g.Mini)
string mini = g != null && !string.IsNullOrEmpty(g.Mini)
? g.Mini!
: 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">
<Grid RowDefinitions="32,48">
<!-- Row 0: title / caption strip (draggable; OS buttons top-right) -->
<TextBlock Grid.Row="0" Text="Starter MaSzyna" VerticalAlignment="Center"
<!-- Row 0: title / caption strip. Transparent background makes the
whole strip a drag handle (PointerPressed -> BeginMoveDrag),
and the OS buttons render at its top-right. -->
<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) -->
<Grid Grid.Row="1" ColumnDefinitions="Auto,Auto,*,Auto">

View File

@@ -5,6 +5,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Threading;
using StarterNG.Classes;
@@ -32,6 +33,13 @@ public partial class MainWindow : Window
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 ──────────
private void Nav_OnCheckedChanged(object? sender, RoutedEventArgs e)
{

View File

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

View File

@@ -171,11 +171,13 @@
<Setter Property="Background" Value="{StaticResource CarbonAccent}" />
</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">
<Setter Property="FontSize" Value="14" />
<Setter Property="MinHeight" Value="0" />
<Setter Property="Padding" Value="10,6" />
<Setter Property="MinHeight" Value="38" />
<Setter Property="Padding" Value="12,4,12,8" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<!-- ── Compact list (tight rows that fill the available space) ─────────── -->
@@ -187,4 +189,14 @@
<Setter Property="MinHeight" Value="0" />
<Setter Property="FontSize" Value="12" />
</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>

View File

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

View File

@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
@@ -40,8 +41,8 @@ public partial class Depot : UserControl
// debounces search input so we don't rebuild on every keystroke
private DispatcherTimer? _searchTimer;
// scenery-consist previews are built lazily on first dropdown open
private bool _consistPreviewsBuilt;
// the scenery whose consists are listed on the right (from the Scenarios tab)
private Scenery? _listScenery;
// active database filters
private Func<string?, bool>? _categoryFilter; // matches a category letter, null = none chosen
@@ -141,16 +142,26 @@ public partial class Depot : UserControl
Dispatcher.UIThread.Post(() =>
{
PopulateCategoryCombo(); // -> populates class combo -> builds browser
PopulateSceneryCombo(); // -> populates consist combo for the first scenery
PopulateSceneryConsists(); // -> lists the current scenery's consists
RebuildConsist();
}, 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();
PropertyChanged += (_, e) =>
{
if (e.Property == IsVisibleProperty && IsVisible)
SyncFromSelection();
};
}
private void SyncFromSelection()
{
// refresh the on-map consists list to the scenery picked in the Scenarios tab
PopulateSceneryConsists();
var trainset = AppState.Instance.CurrentTrainset;
if (trainset != null && !ReferenceEquals(trainset, _editingTrainset))
LoadTrainset(trainset);
@@ -497,116 +508,62 @@ public partial class Depot : UserControl
// ------------------------------------------------------------- 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;
sceneryCombo.Items.Clear();
foreach (var scenery in _sceneries)
{
sceneryCombo.Items.Add(new ComboBoxItem
{
Content = Path.GetFileNameWithoutExtension(scenery.Path),
Tag = scenery
});
}
if (sceneryCombo.Items.Count > 0)
sceneryCombo.SelectedIndex = 0;
_suppress = false;
sceneryConsistList.Items.Clear();
PopulateConsistCombo(_sceneries.FirstOrDefault());
}
_listScenery = AppState.Instance.CurrentScenery ?? _sceneries.FirstOrDefault();
mapSceneryLabel.Text = _listScenery != null
? Path.GetFileNameWithoutExtension(_listScenery.Path)
: App.Loc["NoSceneryConsists"];
private void PopulateConsistCombo(Scenery? scenery)
if (_listScenery != null)
{
_suppress = true;
sceneryConsistCombo.Items.Clear();
if (scenery != null)
{
foreach (var trainset in scenery.Trainsets)
foreach (var trainset in _listScenery.Trainsets)
{
if (trainset.Vehicles.Count == 0)
continue;
if (trainset.Vehicles.All(v => string.IsNullOrWhiteSpace(v.Name)))
continue;
// lightweight text content now; the rich mini-preview is built
// lazily the first time the dropdown is opened
sceneryConsistCombo.Items.Add(new ComboBoxItem
var entry = new ListBoxItem
{
Content = ConsistSummary(trainset),
Content = ConsistText(trainset),
Tag = trainset
});
}
}
};
sceneryConsistList.Items.Add(entry);
_consistPreviewsBuilt = false;
sceneryConsistCombo.SelectedItem = null;
if (ReferenceEquals(trainset, AppState.Instance.CurrentTrainset))
sceneryConsistList.SelectedItem = entry;
}
}
_suppress = false;
}
private static string ConsistSummary(Trainset trainset)
{
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)
// Selecting a consist on the map loads it into the editor below.
private void SceneryConsistList_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (_suppress) return;
var scenery = (sceneryCombo.SelectedItem as ComboBoxItem)?.Tag as Scenery;
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 })
if (sceneryConsistList.SelectedItem is not ListBoxItem { Tag: Trainset trainset })
return;
AppState.Instance.CurrentScenery = (sceneryCombo.SelectedItem as ComboBoxItem)?.Tag as Scenery;
AppState.Instance.CurrentScenery = _listScenery;
AppState.Instance.CurrentTrainset = 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
private Dynamic MakeDynamic(VehicleTexture texture)
@@ -728,7 +685,6 @@ public partial class Depot : UserControl
}
emptyHint.IsVisible = _consist.Count == 0;
UpdateSummary();
UpdateDetails();
WriteBackToScenery();
}
@@ -849,22 +805,123 @@ public partial class Depot : UserControl
Cursor = _hand,
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;
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;
}
// 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)
{
string key = UnitKey(item.Cars[0]);
return item.Cars.Count > 1 ? $"{key} [{item.Cars.Count}]" : key;
}
// Coupling indicator between two units. Hovering shows the 8-bit coupling
// editor for the left unit's last car (placeholder; full editing comes later).
// Coupling indicator between two units. Click it to open a (clickable) flyout
// 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)
{
var glyph = new TextBlock
@@ -875,17 +932,21 @@ public partial class Depot : UserControl
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center
};
var coupler = new Border
var coupler = new Button
{
Width = 18,
Padding = new Thickness(2),
MinWidth = 0,
Cursor = _hand,
Background = Brushes.Transparent,
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]));
ToolTip.SetShowDelay(coupler, 150);
coupler.Classes.Add("Basic");
ToolTip.SetTip(coupler, App.Loc["Coupling"]);
return coupler;
}
@@ -914,40 +975,7 @@ public partial class Depot : UserControl
panel.Children.Add(check);
}
// Brake-rack setting (the "B" parameter of the coupling field).
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);
// The brake setting lives in the Brakes tab now, not here.
return new Border { Padding = new Thickness(8), Child = panel };
}
@@ -983,32 +1011,177 @@ public partial class Depot : UserControl
return btn;
}
private void UpdateSummary()
{
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.
// Brakes / Loads editors for the selected unit (applied to all of its cars).
private void UpdateDetails()
{
brakesPanel.Children.Clear();
loadsPanel.Children.Clear();
if (_selected is null)
{
brakesContent.Text = App.Loc["SelectVehicleHint"];
loadsContent.Text = App.Loc["SelectVehicleHint"];
brakesPanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
loadsPanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
return;
}
var lead = _selected.Cars[0];
string title = _selected.Grouped ? UnitLabel(_selected) : lead.Name;
brakesContent.Text =
$"{App.Loc["Vehicle"]}: {title}\n" +
$"cars: {string.Join(", ", _selected.Cars.Select(c => c.SkinFile))}\n\n" +
$"{App.Loc["ComingSoon"]}";
loadsContent.Text =
$"{App.Loc["Vehicle"]}: {title}\n\n" +
$"{App.Loc["ComingSoon"]}";
BuildBrakesEditor(_selected);
BuildLoadsEditor(_selected);
}
private static TextBlock DetailHint(string text) => new()
{
Text = text, Opacity = 0.6, FontSize = 12, TextWrapping = TextWrapping.Wrap
};
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
@@ -1049,18 +1222,6 @@ public partial class Depot : UserControl
_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.

View File

@@ -21,7 +21,8 @@
<HeaderedContentControl Grid.Column="0" Grid.Row="0" Theme="{StaticResource GroupBox}"
Classes="Compact"
Header="{Binding [Sceneries], Source={x:Static starterNg:App.Loc}}">
<TreeView Name="sceneryList" SelectionChanged="SceneryList_OnSelectionChanged" />
<TreeView Name="sceneryList" Classes="Compact"
SelectionChanged="SceneryList_OnSelectionChanged" />
</HeaderedContentControl>
<!-- 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
// 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();
PropertyChanged += (_, e) =>
{
if (e.Property == IsVisibleProperty && IsVisible)
RefreshSelectedConsist();
};
}
private void SceneryList_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)

View File

@@ -115,6 +115,17 @@
<String key="Brakes">Brakes</String>
<String key="Loads">Loads</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="ComingSoon">Editing options coming soon.</String>
<String key="Split">Split</String>

View File

@@ -115,6 +115,17 @@
<String key="Brakes">Hamulce</String>
<String key="Loads">Ładunki</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="ComingSoon">Opcje edycji wkrótce.</String>
<String key="Split">Rozdziel</String>