WIP: settings sections, shortcuts window, depot tweaks

This commit is contained in:
maj00r
2026-09-04 20:40:47 +02:00
parent bcf266a167
commit aef086e9f0
35 changed files with 2945 additions and 1104 deletions

View File

@@ -10,16 +10,17 @@ using Avalonia.Layout;
using Avalonia.Media;
using StarterNG.Classes;
using StarterNG.Controls;
using StarterNG.Domain;
namespace StarterNG.Views;
public sealed class VehicleDetails
{
private readonly HeaderedContentControl selectedVehiclePanel;
private readonly StackPanel generalPanel;
private readonly StackPanel consistTexturePanel;
private readonly StackPanel couplerPanel;
private readonly Panel couplerActions;
private readonly StackPanel brakesPanel;
private readonly StackPanel loadsPanel;
private readonly StackPanel damagePanel;
@@ -36,17 +37,17 @@ public sealed class VehicleDetails
private readonly Action<VehicleTexture, StackPanel> _showTextureInfo;
public VehicleDetails(
HeaderedContentControl selectedVehiclePanel,
StackPanel generalPanel, StackPanel consistTexturePanel, StackPanel couplerPanel,
Panel couplerActions,
StackPanel brakesPanel, StackPanel loadsPanel, StackPanel damagePanel,
Button removeVehicleButton,
VehicleDatabase db, VehicleInfo info, Consist consist, Cargo cargo, Cursor hand,
Action redraw, Action refreshChrome, Action<VehicleTexture, StackPanel> showTextureInfo)
{
this.selectedVehiclePanel = selectedVehiclePanel;
this.generalPanel = generalPanel;
this.consistTexturePanel = consistTexturePanel;
this.couplerPanel = couplerPanel;
this.couplerActions = couplerActions;
this.brakesPanel = brakesPanel;
this.loadsPanel = loadsPanel;
this.damagePanel = damagePanel;
@@ -67,31 +68,27 @@ public sealed class VehicleDetails
generalPanel.Children.Clear();
consistTexturePanel.Children.Clear();
couplerPanel.Children.Clear();
couplerActions.Children.Clear();
brakesPanel.Children.Clear();
loadsPanel.Children.Clear();
damagePanel.Children.Clear();
selectedVehiclePanel.Header = _consist.Selected is null
? App.Loc["SelectedVehicle"]
: $"{App.Loc["SelectedVehicle"]}: " +
(_consist.Selected.Grouped ? Consist.UnitLabel(_consist.Selected) : _consist.Selected.Cars[0].Name);
if (_consist.Selected != null && !_consist.Selected.Cars.Contains(_consist.SelectedCar!))
_consist.SelectedCar = _consist.Selected.Cars[0];
_refreshChrome();
loadsPanel.Children.Add(BuildConsistLoadTools());
if (_consist.Selected is null)
{
generalPanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
consistTexturePanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
couplerPanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
brakesPanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
loadsPanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
damagePanel.Children.Add(DetailHint(App.Loc["SelectVehicleHint"]));
generalPanel.Children.Add(PanelNote(App.Loc["SelectVehicleHint"]));
consistTexturePanel.Children.Add(PanelNote(App.Loc["SelectVehicleHint"]));
couplerPanel.Children.Add(PanelNote(App.Loc["SelectVehicleHint"]));
brakesPanel.Children.Add(PanelNote(App.Loc["SelectVehicleHint"]));
loadsPanel.Children.Add(PanelNote(App.Loc["SelectVehicleHint"]));
damagePanel.Children.Add(PanelNote(App.Loc["SelectVehicleHint"]));
removeVehicleButton.IsEnabled = false;
// The whole-consist tools still apply with nothing selected.
AddConsistLoadSection();
return;
}
@@ -100,21 +97,27 @@ public sealed class VehicleDetails
if (_db.TextureForSkin(_consist.ActiveCar(_consist.Selected).SkinFile) is { } tex)
_showTextureInfo(tex, consistTexturePanel);
else
consistTexturePanel.Children.Add(DetailHint(App.Loc["NoTextureInfo"]));
consistTexturePanel.Children.Add(PanelNote(App.Loc["NoTextureInfo"]));
removeVehicleButton.IsEnabled = true;
if (_consist.Selected.Cars.Count > 1)
{
couplerPanel.Children.Add(UnitScopeHint(App.Loc["CouplingUnitRear"]));
brakesPanel.Children.Add(UnitScopeHint(App.Loc["AppliesToUnit"]));
couplerPanel.Children.Add(PanelNote(App.Loc["CouplingUnitRear"]));
brakesPanel.Children.Add(PanelNote(App.Loc["AppliesToUnit"]));
}
BuildCouplerEditor(_consist.Selected);
BuildBrakesEditor(_consist.Selected);
BuildLoadsEditor(_consist.Selected);
BuildConfigExtras(_consist.Selected);
BuildDamageEditor(_consist.Selected);
// Configuration tab: what this vehicle is, what it carries, then the actions
// that reach past it - each under a header that says which is which.
loadsPanel.Children.Add(
Section(App.Loc["Vehicle"], BuildVehicleSection(_consist.Selected)));
loadsPanel.Children.Add(
Section(App.Loc["Load"], BuildLoadSection(_consist.Selected)));
AddConsistLoadSection();
}
private void BuildGeneralInfo(ConsistItem item)
@@ -122,6 +125,9 @@ public sealed class VehicleDetails
var car = _consist.ActiveCar(item);
var inv = CultureInfo.InvariantCulture;
if (!string.IsNullOrWhiteSpace(car.Name))
generalPanel.Children.Add(Reading(App.Loc["Name"], car.Name));
bool unit = item.Cars.Count > 1;
var members = item.Cars.Select(_info.PhysicsFor).Where(p => p != null).ToList();
@@ -132,49 +138,19 @@ public sealed class VehicleDetails
double? vmax = members.Count == 0 ? null
: unit ? members.Min(p => p!.VMax) : members[0]!.VMax;
generalPanel.Children.Add(InfoRow($"{App.Loc["Length"]} [m]:",
generalPanel.Children.Add(Reading($"{App.Loc["Length"]} [m]",
length?.ToString("0.##", inv)));
generalPanel.Children.Add(InfoRow($"{App.Loc["Mass"]} [t]:",
generalPanel.Children.Add(Reading($"{App.Loc["Mass"]} [t]",
mass?.ToString("0.#", inv)));
generalPanel.Children.Add(InfoRow($"{App.Loc["VMax"]} [km/h]:",
generalPanel.Children.Add(Reading($"{App.Loc["VMax"]} [km/h]",
vmax?.ToString("0", inv)));
if (unit)
generalPanel.Children.Add(InfoRow($"{App.Loc["VehicleCount"]}:",
generalPanel.Children.Add(Reading(App.Loc["VehicleCount"],
item.Cars.Count.ToString(inv)));
generalPanel.Children.Add(InfoRow($"{App.Loc["Model"]}:", car.MmdFile));
generalPanel.Children.Add(InfoRow($"{App.Loc["Texture"]}:", car.SkinFile));
}
private static Control InfoRow(string label, string? value)
{
var grid = new Grid { ColumnDefinitions = new ColumnDefinitions("Auto,10,*") };
var caption = new TextBlock
{
Text = label,
FontSize = 12,
MinWidth = 110,
TextAlignment = TextAlignment.Right,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center
};
var reading = new TextBlock
{
Text = string.IsNullOrWhiteSpace(value) ? "–" : value,
FontSize = 12,
TextAlignment = TextAlignment.Right,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center,
TextWrapping = TextWrapping.Wrap
};
Grid.SetColumn(reading, 2);
grid.Children.Add(caption);
grid.Children.Add(reading);
return grid;
generalPanel.Children.Add(Reading(App.Loc["Model"], car.MmdFile));
generalPanel.Children.Add(Reading(App.Loc["Texture"], car.SkinFile));
}
private void BuildCouplerEditor(ConsistItem item)
@@ -187,12 +163,17 @@ public sealed class VehicleDetails
var d = Consist.TailCar(item);
var grid = new Grid
// Two columns: the tab is wide enough and the list is short enough
// to avoid scrolling while still reading left-to-right, top-to-bottom.
var list = new Grid
{
ColumnDefinitions = new ColumnDefinitions("*,8,*"),
RowDefinitions = new RowDefinitions("Auto,Auto,Auto,Auto"),
RowSpacing = 4
ColumnDefinitions = new ColumnDefinitions("*,*"),
RowSpacing = 2,
ColumnSpacing = 16
};
for (int r = 0; r < (CouplingBits.BitKeys.Length + 1) / 2; r++)
list.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
for (int i = 0; i < CouplingBits.BitKeys.Length; i++)
{
int bit = 1 << i;
@@ -207,17 +188,16 @@ public sealed class VehicleDetails
d.Coupling.Set(bit, check.IsChecked == true);
_redraw();
};
int half = (CouplingBits.BitKeys.Length + 1) / 2;
Grid.SetColumn(check, i < half ? 0 : 2);
Grid.SetRow(check, i % half);
grid.Children.Add(check);
Grid.SetRow(check, i / 2);
Grid.SetColumn(check, i % 2);
list.Children.Add(check);
}
couplerPanel.Children.Add(grid);
couplerPanel.Children.Add(list);
var copy = new Button
{
Content = App.Loc["CopyCoupler"],
FontSize = 11,
FontSize = 12,
Cursor = _hand,
HorizontalAlignment = HorizontalAlignment.Stretch,
HorizontalContentAlignment = HorizontalAlignment.Center,
@@ -235,7 +215,7 @@ public sealed class VehicleDetails
var auto = new Button
{
Content = App.Loc["AutoCoupler"],
FontSize = 11,
FontSize = 12,
Cursor = _hand,
HorizontalAlignment = HorizontalAlignment.Stretch,
HorizontalContentAlignment = HorizontalAlignment.Center,
@@ -250,19 +230,23 @@ public sealed class VehicleDetails
Refresh();
};
var buttons = new Grid
// Laid out horizontally under the checkbox list so the grid can use the full tab width
var buttons = new StackPanel
{
ColumnDefinitions = new ColumnDefinitions("125*,4,48*"),
Orientation = Orientation.Horizontal,
Spacing = 4,
Margin = new Thickness(0, 6, 0, 0)
};
Grid.SetColumn(copy, 0);
Grid.SetColumn(auto, 2);
buttons.Children.Add(copy);
buttons.Children.Add(auto);
couplerPanel.Children.Add(buttons);
couplerActions.Children.Add(buttons);
}
private void BuildConfigExtras(ConsistItem item)
/// <summary>
/// The vehicle's own settings. Every write path is unchanged: crew and wagon number
/// go through the unit's active car, the coolant flag to its first car only.
/// </summary>
private Control BuildVehicleSection(ConsistItem item)
{
var driver = new ComboBox { FontSize = 12, MinWidth = 0 };
foreach (string key in new[] { "DriverHead", "DriverRear", "DriverPassenger", "DriverNobody" })
@@ -296,75 +280,143 @@ public sealed class VehicleDetails
};
driver.MinWidth = 0;
driver.HorizontalAlignment = HorizontalAlignment.Stretch;
loadsPanel.Children.Insert(0, LabeledRow(App.Loc["CrewLabel"], driver, labelWidth: 70));
var reversed = new CheckBox
{
Content = App.Loc["Reversed"],
IsChecked = item.Flipped,
FontSize = 11
};
reversed.IsCheckedChanged += (_, _) =>
{
if ((reversed.IsChecked == true) != item.Flipped)
_consist.Flip(item);
};
var rows = new StackPanel();
rows.Children.Add(Row(App.Loc["CrewLabel"], driver));
if (IsPassengerVehicle(item))
{
var lead = item.Cars[0];
var numBox = new NumericUpDown
{
FontSize = 12,
Minimum = 0,
Maximum = 99999,
Increment = 1,
Value = Consist.GetWagonNumber(lead),
FormatString = "0"
};
numBox.ValueChanged += (_, _) =>
{
_consist.SetWagonNumber(lead, (int)(numBox.Value ?? 0));
_redraw();
};
rows.Children.Add(Row(App.Loc["WagonNumber"], numBox));
}
// Orientation is not here any more: it reads and toggles off the flip button on
// the vehicle's own card, which lights up when the vehicle sits reversed.
// The flag rides on the unit's first car, so that is the car that has to own a
// diesel engine for it to mean anything: the simulator runs its heat model only
// for DieselEngine and DieselElectric, and on anything else ".TA" is written into
// the scenario and then ignored. Shown anyway when the flag is already set, so an
// inherited one stays visible and removable.
var d = item.Cars[0];
var thermo = new CheckBox
{
Content = App.Loc["ThermoAmbient"],
IsChecked = d.Coupling.ThermoDynamic,
FontSize = 11
};
thermo.IsCheckedChanged += (_, _) => d.Coupling.ThermoDynamic = thermo.IsChecked == true;
bool thermoApplies = _info.PhysicsFor(d)?.IsDieselEngine == true
|| d.Coupling.ThermoDynamic;
var switches = new Grid
if (thermoApplies)
{
ColumnDefinitions = new ColumnDefinitions("*,8,*"),
Margin = new Thickness(0, 4, 0, 0)
};
Grid.SetColumn(thermo, 2);
switches.Children.Add(reversed);
switches.Children.Add(thermo);
loadsPanel.Children.Add(switches);
var thermo = new CheckBox
{
Content = App.Loc["ThermoAmbient"],
IsChecked = d.Coupling.ThermoDynamic,
FontSize = 12
};
thermo.IsCheckedChanged += (_, _) => d.Coupling.ThermoDynamic = thermo.IsChecked == true;
// Label-less row, so the box indents to the control column with everything
// else instead of starting at the panel's left edge on its own.
rows.Children.Add(Row(content: thermo));
}
return rows;
}
private static TextBlock UnitScopeHint(string text) => new()
private bool IsPassengerVehicle(ConsistItem item)
{
var lead = _consist.ActiveCar(item);
var phys = _info.PhysicsFor(lead);
if (phys == null)
return false;
return phys.LoadAccepted.Split(',', ';')
.Select(s => s.Trim())
.Where(s => s.Length > 0)
.Any(s => s.Equals("passengers", StringComparison.OrdinalIgnoreCase));
}
/// <summary>
/// The one row shape every detail tab uses. The label column negotiates its width
/// through the shared-size group on the host panel, so all rows in a panel line up
/// without anyone naming a pixel count - see the Compact styles in Settings.axaml.
/// Omitting <paramref name="content"/> makes it a read-only line; omitting
/// <paramref name="label"/> indents a bare widget to the control column.
/// </summary>
private static SettingRow Row(string? label = null, Control? content = null,
string? value = null, string? description = null,
string? tip = null)
{
var row = new SettingRow
{
Label = label,
Description = description,
ValueText = value,
Content = content
};
row.Classes.Add("Compact");
if (!string.IsNullOrWhiteSpace(tip))
ToolTip.SetTip(row, tip);
return row;
}
/// <summary>
/// A read-only label/value line. The dash keeps the row from collapsing when a
/// vehicle has no reading for the field.
/// </summary>
private static SettingRow Reading(string label, string? value)
{
bool empty = string.IsNullOrWhiteSpace(value);
return Row(label, value: empty ? "\u2013" : value, tip: empty ? null : value);
}
private static SettingsSection Section(string header, Control content, Control? meta = null)
{
var section = new SettingsSection
{
Header = header,
Content = content,
// Passive text only: Toggle sits outside the logical tree, so nothing put
// here can be found later with GetLogicalDescendants.
Toggle = meta
};
section.Classes.Add("Compact");
return section;
}
private static StackPanel Stack(params Control[] children)
{
var panel = new StackPanel { Spacing = 3 };
foreach (var c in children)
panel.Children.Add(c);
return panel;
}
private static readonly IBrush DimBrush = new SolidColorBrush(Color.Parse("#9098A0"));
/// <summary>
/// A note that governs a whole panel or section rather than one row - same look as a
/// row's description line. Replaces the old DetailHint/UnitScopeHint pair, which
/// differed only by a 2 px margin.
/// </summary>
private static TextBlock PanelNote(string text) => new()
{
Text = text,
FontSize = 11,
Opacity = 0.6,
FontSize = 12,
Foreground = DimBrush,
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(0, 0, 0, 2)
Margin = new Thickness(0, 0, 0, 8)
};
private static TextBlock DetailHint(string text) => new()
{
Text = text, Opacity = 0.6, FontSize = 12, TextWrapping = TextWrapping.Wrap
};
private static Control LabeledRow(string label, Control control, double labelWidth = 120)
{
var grid = new Grid { ColumnDefinitions = new ColumnDefinitions("Auto,8,*") };
var caption = new TextBlock
{
Text = label,
FontSize = 12,
MinWidth = labelWidth,
TextAlignment = TextAlignment.Right,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center
};
control.HorizontalAlignment = HorizontalAlignment.Stretch;
Grid.SetColumn(control, 2);
grid.Children.Add(caption);
grid.Children.Add(control);
return grid;
}
private static void AddOption(ComboBox combo, string content, string? tag)
=> combo.Items.Add(new ComboBoxItem { Content = content, Tag = tag });
@@ -402,9 +454,9 @@ public sealed class VehicleDetails
switchCombo.SelectionChanged += (_, _) => Apply();
loadCombo.SelectionChanged += (_, _) => Apply();
brakesPanel.Children.Add(LabeledRow(App.Loc["BrakeMode"], modeCombo, labelWidth: 96));
brakesPanel.Children.Add(LabeledRow(App.Loc["BrakeSwitch"], switchCombo, labelWidth: 96));
brakesPanel.Children.Add(LabeledRow(App.Loc["BrakeLoad"], loadCombo, labelWidth: 96));
brakesPanel.Children.Add(Row(App.Loc["BrakeMode"], modeCombo));
brakesPanel.Children.Add(Row(App.Loc["BrakeSwitch"], switchCombo));
brakesPanel.Children.Add(Row(App.Loc["BrakeLoad"], loadCombo));
}
private static string BrakeModeLabel(string? code) => code switch
@@ -413,7 +465,6 @@ public sealed class VehicleDetails
"P" => App.Loc["BrakePassenger"],
"R" => App.Loc["BrakeExpress"],
"M" => App.Loc["BrakeExpressMg"],
"Q" => App.Loc["BrakeNoAir"],
_ => App.Loc["BrakeActingNone"]
};
@@ -427,8 +478,12 @@ public sealed class VehicleDetails
private static string SwitchLabel(string? code) => code switch
{
"<>" => App.Loc["SwitchEmergencyTest"],
"0" => App.Loc["SwitchOff"],
"1" => App.Loc["SwitchOff10"],
"X" => App.Loc["SwitchAgonal"],
"E" => App.Loc["SwitchEmptied"],
"Q" => App.Loc["BrakeNoAir"],
_ => App.Loc["SwitchNormal"]
};
@@ -439,7 +494,7 @@ public sealed class VehicleDetails
NumericUpDown Spin(int value, int max) => new()
{
FontSize = 12, Minimum = 0, Maximum = max, Increment = 1,
Value = value, MinWidth = 0, Width = 100, FormatString = "0"
Value = value, MinWidth = 0, FormatString = "0"
};
var sway = Spin(w.Sway, 100);
@@ -465,84 +520,100 @@ public sealed class VehicleDetails
flatRand.ValueChanged += (_, _) => Apply();
flatProb.ValueChanged += (_, _) => Apply();
var grid = new Grid
{
ColumnDefinitions = new ColumnDefinitions("*,8,Auto"),
RowSpacing = 6
};
void Row(string label, Control control)
{
int r = grid.RowDefinitions.Count;
grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
var caption = new TextBlock
{
Text = label, FontSize = 12,
TextWrapping = TextWrapping.Wrap,
TextAlignment = TextAlignment.Right,
VerticalAlignment = VerticalAlignment.Center
};
Grid.SetRow(caption, r);
Grid.SetColumn(caption, 0);
control.HorizontalAlignment = HorizontalAlignment.Right;
Grid.SetRow(control, r);
Grid.SetColumn(control, 2);
grid.Children.Add(caption);
grid.Children.Add(control);
}
Row(App.Loc["DamageSway"], sway);
Row(App.Loc["DamageFlatness"], flat);
Row(App.Loc["DamageFlatnessRand"], flatRand);
Row(App.Loc["DamageFlatnessProb"], flatProb);
damagePanel.Children.Add(grid);
damagePanel.Children.Add(Row(App.Loc["DamageSway"], sway));
damagePanel.Children.Add(Row(App.Loc["DamageFlatness"], flat));
damagePanel.Children.Add(Row(App.Loc["DamageFlatnessRand"], flatRand));
damagePanel.Children.Add(Row(App.Loc["DamageFlatnessProb"], flatProb));
}
private Control BuildConsistLoadTools()
/// <summary>
/// The three actions that fill the selected vehicle from its neighbours. They keep
/// the primary (filled) look because they act on what you have selected.
/// </summary>
private Control BuildVehicleLoadTools(ConsistItem sel)
{
var panel = new StackPanel { Spacing = 4, Margin = new Thickness(0, 0, 0, 4) };
var row = new WrapPanel { Orientation = Orientation.Horizontal };
row.Children.Add(LoadToolButton(App.Loc["ConsistRandomType"],
() => { _cargo.RandomTypes(_consist); _redraw(); }));
row.Children.Add(LoadToolButton(App.Loc["ConsistMaxAmount"],
() => { _cargo.MaxAmounts(_consist); _redraw(); }));
row.Children.Add(LoadToolButton(App.Loc["ConsistRandomAmount"],
() => { _cargo.RandomAmounts(_consist); _redraw(); }));
panel.Children.Add(row);
return panel;
bool loadable = _cargo.IsLoadable(sel);
var prev = LoadToolButton(App.Loc["LoadPrevShort"],
() => { if (_consist.Selected is { } s) { _cargo.CopyFromPrevious(_consist, s); _redraw(); } });
prev.IsEnabled = loadable && _consist.IndexOf(sel) > 0;
ToolTip.SetTip(prev, App.Loc["LoadCopyPrev"]);
var max = LoadToolButton(App.Loc["LoadMaxShort"],
() => { if (_consist.Selected is { } s) { _cargo.FillUnit(s); _redraw(); } });
max.IsEnabled = loadable;
ToolTip.SetTip(max, App.Loc["LoadMax"]);
var toAll = LoadToolButton(App.Loc["LoadToAllShort"],
() => { if (_consist.Selected is { } s) { _cargo.CopyToFollowing(_consist, s); _redraw(); } });
toAll.IsEnabled = loadable;
ToolTip.SetTip(toAll, App.Loc["LoadCopyToAll"]);
return Stack(prev, max, toAll);
}
private Button LoadToolButton(string text, Action onClick)
/// <summary>
/// The whole-consist actions. Secondary styling plus a header that names the scope
/// and counts the vehicles, because these reach far past the selected card. No
/// tooltips: theirs used to repeat the button label word for word.
/// </summary>
private void AddConsistLoadSection()
{
if (_consist.Count == 0)
return;
var buttons = Stack(
LoadToolButton(App.Loc["ConsistRandomType"],
() => { _cargo.RandomTypes(_consist); _redraw(); }, secondary: true),
LoadToolButton(App.Loc["ConsistMaxAmount"],
() => { _cargo.MaxAmounts(_consist); _redraw(); }, secondary: true),
LoadToolButton(App.Loc["ConsistRandomAmount"],
() => { _cargo.RandomAmounts(_consist); _redraw(); }, secondary: true));
var count = new TextBlock
{
Text = $"{App.Loc["VehicleCount"]}: {_consist.Count}",
FontSize = 12,
Foreground = DimBrush,
VerticalAlignment = VerticalAlignment.Center
};
loadsPanel.Children.Add(Section(App.Loc["ConsistLoadTools"], buttons, count));
}
private Button LoadToolButton(string text, Action onClick, bool secondary = false)
{
var b = new Button
{
Content = text, FontSize = 11, Margin = new Thickness(0, 0, 4, 4),
Padding = new Thickness(8, 3), Cursor = _hand,
Content = text, FontSize = 12,
Padding = new Thickness(6, 2), Cursor = _hand,
HorizontalAlignment = HorizontalAlignment.Stretch,
HorizontalContentAlignment = HorizontalAlignment.Center,
IsEnabled = _consist.Count > 0
};
b.Classes.Add("Flat");
b.Classes.Add(secondary ? "Outline" : "Flat");
b.Click += (_, _) => onClick();
return b;
}
private void BuildLoadsEditor(ConsistItem item)
/// <summary>
/// What the selected vehicle carries, and the actions that fill it. A vehicle that
/// takes no cargo gets the note alone - previously the three actions stayed behind,
/// greyed out and useless.
/// </summary>
private Control BuildLoadSection(ConsistItem item)
{
var lead = _consist.ActiveCar(item);
var available = _cargo.AcceptedTypes(lead);
if (available.Count == 0 && string.IsNullOrEmpty(lead.LoadType))
{
loadsPanel.Children.Add(DetailHint(App.Loc["LoadNotLoadable"]));
return;
}
return PanelNote(App.Loc["LoadNotLoadable"]);
if (!string.IsNullOrEmpty(lead.LoadType) &&
!available.Contains(lead.LoadType!, StringComparer.OrdinalIgnoreCase))
available.Insert(0, lead.LoadType!);
var typeCombo = new ComboBox { FontSize = 12, MinWidth = 0 };
var typeCombo = new ComboBox { FontSize = 12, MinWidth = 120 };
typeCombo.Items.Add(new ComboBoxItem { Content = App.Loc["None"], Tag = null });
foreach (var name in available)
typeCombo.Items.Add(new ComboBoxItem { Content = LoadWeights.Describe(name), Tag = name });
@@ -557,41 +628,129 @@ public sealed class VehicleDetails
break;
}
string? SelectedType() => (typeCombo.SelectedItem as ComboBoxItem)?.Tag as string;
bool IsPant() => Dynamic.IsPantStateType(SelectedType());
int limit = _cargo.LimitFor(lead);
var countBox = new NumericUpDown
{
FontSize = 12, Minimum = 0, Maximum = limit, Increment = 1,
Value = Math.Min(lead.LoadCount, limit), MinWidth = 120, FormatString = "0"
Value = Math.Min(lead.LoadCount, limit), FormatString = "0",
HorizontalAlignment = HorizontalAlignment.Stretch
};
var pantHint = DetailHint(App.Loc["LoadPantStateHint"]);
pantHint.IsVisible = lead.IsPantState;
var pantStateCombo = new ComboBox
{
FontSize = 12, MinWidth = 0,
HorizontalAlignment = HorizontalAlignment.Stretch
};
pantStateCombo.Items.Add(new ComboBoxItem { Content = App.Loc["PantStateNone"], Tag = 0 });
pantStateCombo.Items.Add(new ComboBoxItem { Content = "A", Tag = 1 });
pantStateCombo.Items.Add(new ComboBoxItem { Content = "B", Tag = 2 });
pantStateCombo.Items.Add(new ComboBoxItem { Content = App.Loc["PantStateBoth"], Tag = 3 });
string? SelectedType() => (typeCombo.SelectedItem as ComboBoxItem)?.Tag as string;
var countContainer = new Panel();
countContainer.Children.Add(countBox);
countContainer.Children.Add(pantStateCombo);
string UnitText()
{
var phys = _info.PhysicsFor(lead);
if (phys == null) return App.Loc["LoadUnitGeneric"];
if (string.Equals(phys.LoadQ, "pieces", StringComparison.OrdinalIgnoreCase))
return App.Loc["LoadUnitPieces"];
if (string.Equals(phys.LoadQ, "tonns", StringComparison.OrdinalIgnoreCase))
return App.Loc["LoadUnitTons"];
return App.Loc["LoadUnitGeneric"];
}
var countRow = Row(App.Loc["LoadCount"], countContainer);
void SyncPantStateFromCount()
{
int value = Math.Clamp((int)(countBox.Value ?? 0), 0, Dynamic.PantStateMax);
foreach (var obj in pantStateCombo.Items)
if (obj is ComboBoxItem ci && ci.Tag is int t && t == value)
{
pantStateCombo.SelectedItem = ci;
return;
}
}
void SyncCountFromPantState()
{
int value = pantStateCombo.SelectedItem is ComboBoxItem { Tag: int t } ? t : 0;
countBox.Value = Math.Clamp(value, 0, countBox.Maximum);
}
void UpdateVisibility()
{
bool pant = IsPant();
countBox.IsVisible = !pant;
pantStateCombo.IsVisible = pant;
// The unit slot is reserved by the Compact style even when empty, so the
// spinner does not jump sideways as the unit comes and goes.
countRow.Label = pant ? App.Loc["Pantograph"] : App.Loc["LoadCount"];
countRow.ValueText = pant ? null : UnitText();
// The "set an amount above 0" advice is only true while the amount is 0;
// past that it is a line of noise in a panel with no height to spare.
countRow.Description = pant ? App.Loc["LoadPantStateHint"]
: GetCount() == 0 ? App.Loc["LoadHint"]
: null;
}
int GetCount()
{
if (IsPant())
return pantStateCombo.SelectedItem is ComboBoxItem { Tag: int t } ? t : 0;
return (int)(countBox.Value ?? 0);
}
void Apply()
{
string? type = SelectedType();
int count = (int)(countBox.Value ?? 0);
int count = GetCount();
foreach (var c in _consist.MemberTargets(item))
{
c.LoadType = string.IsNullOrEmpty(type) ? null : type;
c.LoadCount = string.IsNullOrEmpty(type) ? 0 : Math.Min(count, _cargo.LimitFor(c, type));
}
}
// Seed the pantograph combo from the model before any handler is listening.
SyncPantStateFromCount();
bool wasPant = IsPant();
typeCombo.SelectionChanged += (_, _) =>
{
string? type = SelectedType();
pantHint.IsVisible = Dynamic.IsPantStateType(type);
countBox.Maximum = _cargo.LimitFor(lead, type);
countBox.Maximum = _cargo.LimitFor(lead, SelectedType());
// Both widgets edit the same LoadCount, so the value is carried across only
// when the mode actually flips. Doing it unconditionally - as UpdateVisibility
// used to - reset the amount to zero on every rebuild, which is why setting
// the maximum load appeared to do nothing: the model was written, the panel
// refreshed, and the refresh immediately wrote zero back.
bool pant = IsPant();
if (pant != wasPant)
{
if (pant)
SyncPantStateFromCount();
else
SyncCountFromPantState();
wasPant = pant;
}
UpdateVisibility();
Apply();
};
countBox.ValueChanged += (_, _) => Apply();
pantStateCombo.SelectionChanged += (_, _) => Apply();
countBox.ValueChanged += (_, _) => { Apply(); UpdateVisibility(); };
loadsPanel.Children.Add(LabeledRow(App.Loc["LoadType"], typeCombo));
loadsPanel.Children.Add(LabeledRow(App.Loc["LoadCount"], countBox));
loadsPanel.Children.Add(pantHint);
UpdateVisibility();
return Stack(
Row(App.Loc["LoadType"], typeCombo),
countRow,
BuildVehicleLoadTools(item));
}
}