diff --git a/StarterNG.Tests/CouplerLookTests.cs b/StarterNG.Tests/CouplerLookTests.cs
new file mode 100644
index 0000000..6573914
--- /dev/null
+++ b/StarterNG.Tests/CouplerLookTests.cs
@@ -0,0 +1,25 @@
+using StarterNG.Classes;
+using StarterNG.Views;
+
+namespace StarterNG.Tests;
+
+public class CouplerLookTests
+{
+ [Theory]
+ [InlineData("0", CouplerState.Free)]
+ [InlineData("3", CouplerState.Coupled)]
+ [InlineData("7", CouplerState.MultipleUnit)]
+ [InlineData("131", CouplerState.Permanent)]
+ [InlineData("-3", CouplerState.Permanent)]
+ [InlineData("0.BP", CouplerState.Free)]
+ public void A_coupler_between_vehicles_reads_by_what_it_connects(string code, CouplerState expected) =>
+ Assert.Equal(expected, CouplerLook.StateOf(Coupling.Parse(code), trailing: false));
+
+ [Theory]
+ [InlineData("0", CouplerState.EndSignals)]
+ [InlineData("0.BP", CouplerState.EndSignals)]
+ [InlineData("3", CouplerState.CoupledBehind)]
+ [InlineData("-7", CouplerState.CoupledBehind)]
+ public void The_back_of_the_train_gets_end_signals_only_at_0(string code, CouplerState expected) =>
+ Assert.Equal(expected, CouplerLook.StateOf(Coupling.Parse(code), trailing: true));
+}
diff --git a/StarterNG/Views/Depot/CouplerLook.cs b/StarterNG/Views/Depot/CouplerLook.cs
new file mode 100644
index 0000000..29919b4
--- /dev/null
+++ b/StarterNG/Views/Depot/CouplerLook.cs
@@ -0,0 +1,95 @@
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Media;
+using StarterNG.Classes;
+
+namespace StarterNG.Views;
+
+public enum CouplerState
+{
+ /// Nothing connected between the two vehicles.
+ Free,
+
+ /// Coupled the ordinary way: mechanically, the brake pipe and so on.
+ Coupled,
+
+ /// Coupled with multiple-unit control.
+ MultipleUnit,
+
+ /// Not to be parted in shunting: workshop lock, or a locked coupler.
+ Permanent,
+
+ /// The back of the train at 0 - the simulator puts up the end signals.
+ EndSignals,
+
+ /// The back of the train coupled to whatever stands behind it - no end signals.
+ CoupledBehind
+}
+
+/// How a coupler is drawn on the consist strip, so its state reads at a glance.
+public static class CouplerLook
+{
+ private static readonly IBrush Dim = new SolidColorBrush(Color.Parse("#808080"));
+ private static readonly IBrush Green = new SolidColorBrush(Color.Parse("#41C400"));
+ private static readonly IBrush Blue = new SolidColorBrush(Color.Parse("#4AA3FF"));
+ private static readonly IBrush Red = new SolidColorBrush(Color.Parse("#E5484D"));
+ private static readonly IBrush Amber = new SolidColorBrush(Color.Parse("#E0A030"));
+
+ public static CouplerState StateOf(Coupling coupling, bool trailing)
+ {
+ int flags = coupling.AbsFlags;
+ if (trailing)
+ return flags == 0 ? CouplerState.EndSignals : CouplerState.CoupledBehind;
+ if (flags == 0)
+ return CouplerState.Free;
+ if (coupling.Locked || (flags & Coupling.WorkshopLock) != 0)
+ return CouplerState.Permanent;
+ return (flags & Coupling.ControlMU) != 0 ? CouplerState.MultipleUnit : CouplerState.Coupled;
+ }
+
+ // Box drawing and a plain disc: Windows draws no colour emoji for these.
+ public static string Glyph(CouplerState state) => state switch
+ {
+ CouplerState.Free => "╎",
+ CouplerState.Permanent => "═",
+ CouplerState.EndSignals => "●",
+ _ => "≣"
+ };
+
+ /// The colour of the glyph; null keeps the ordinary text colour.
+ public static IBrush? Brush(CouplerState state) => state switch
+ {
+ CouplerState.Free => Dim,
+ CouplerState.MultipleUnit => Green,
+ CouplerState.Permanent => Blue,
+ CouplerState.EndSignals => Red,
+ CouplerState.CoupledBehind => Amber,
+ _ => null
+ };
+
+ /// What the coupler connects, by the names the coupler editor uses.
+ public static IReadOnlyList Connections(Coupling coupling) =>
+ Enumerable.Range(0, CouplingBits.BitKeys.Length)
+ .Where(i => coupling.Has(1 << i))
+ .Select(i => App.Loc[CouplingBits.BitKeys[i]])
+ .ToList();
+
+ /// The tooltip: the code as the scenery has it, then what it means.
+ public static string Describe(Coupling coupling, bool trailing)
+ {
+ string connections = string.Join(", ", Connections(coupling));
+ string meaning = StateOf(coupling, trailing) switch
+ {
+ CouplerState.Free => App.Loc["CouplerFree"],
+ CouplerState.EndSignals => App.Loc["CouplerEndSignals"],
+ CouplerState.CoupledBehind => string.Format(App.Loc["CouplerCoupledBehind"], connections),
+ _ => string.Format(App.Loc["CouplerConnects"], connections)
+ };
+
+ var lines = new List { string.Format(App.Loc["CouplerCode"], coupling.Flags), meaning };
+ if (coupling.Locked && coupling.AbsFlags != 0)
+ lines.Add(App.Loc["CouplerLocked"]);
+ lines.Add(App.Loc["CouplerClickToEdit"]);
+ return string.Join("\n", lines);
+ }
+}
diff --git a/StarterNG/Views/Depot/VehicleCards.cs b/StarterNG/Views/Depot/VehicleCards.cs
index e3c03b9..08a0dd5 100644
--- a/StarterNG/Views/Depot/VehicleCards.cs
+++ b/StarterNG/Views/Depot/VehicleCards.cs
@@ -406,13 +406,16 @@ public sealed class VehicleCards
flyout.ShowAt(anchor);
}
+ ///
+ /// The coupler after a unit, drawn by its state - free, coupled, multiple-unit,
+ /// permanent, and at the back of the train whether the end signals go up.
+ ///
public Control BuildCoupler(ConsistItem item, bool trailing = false)
{
+ var coupling = Consist.TailCar(item).Coupling;
var glyph = new TextBlock
{
- Text = trailing ? "╡" : "≣",
FontSize = 18,
- Opacity = trailing ? 0.55 : 0.7,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center
};
@@ -422,24 +425,55 @@ public sealed class VehicleCards
MinWidth = 0,
Cursor = _hand,
VerticalAlignment = VerticalAlignment.Center,
- Content = glyph,
- Flyout = new Flyout
- {
- Content = BuildCouplingBox(item),
- Placement = PlacementMode.Top
- }
+ Content = glyph
};
coupler.Classes.Add("Basic");
- ToolTip.SetTip(coupler, $"{App.Loc["Coupling"]} — {App.Loc["TipReorder"]}");
+
+ void Restyle()
+ {
+ var state = CouplerLook.StateOf(coupling, trailing);
+ glyph.Text = CouplerLook.Glyph(state);
+ if (CouplerLook.Brush(state) is { } brush)
+ {
+ glyph.Foreground = brush;
+ glyph.Opacity = 1;
+ }
+ else
+ {
+ glyph.ClearValue(TextBlock.ForegroundProperty);
+ glyph.Opacity = 0.7;
+ }
+ ToolTip.SetTip(coupler, CouplerLook.Describe(coupling, trailing));
+ }
+
+ Restyle();
+ coupler.Flyout = new Flyout
+ {
+ Content = BuildCouplingBox(item, Restyle),
+ Placement = PlacementMode.Top
+ };
return coupler;
}
- private Control BuildCouplingBox(ConsistItem item)
+ ///
+ /// Edits the coupler in place, so the flyout stays open while bits are
+ /// ticked; redraws the coupler it hangs off.
+ ///
+ private Control BuildCouplingBox(ConsistItem item, Action changed)
{
var d = Consist.TailCar(item);
int unitIdx = _consist.IndexOf(item);
var panel = new StackPanel { Spacing = 2 };
+ var checks = new List<(CheckBox Check, int Bit)>();
+ bool syncing = false;
+
+ void Changed()
+ {
+ d.CouplerSetByHand = true;
+ changed();
+ _refreshDetails();
+ }
var copy = new Button
{
@@ -456,7 +490,11 @@ public sealed class VehicleCards
{
if (unitIdx <= 0) return;
d.Coupling.Flags = Consist.TailCar(_consist[unitIdx - 1]).Coupling.Flags;
- _redraw();
+ syncing = true;
+ foreach (var (check, bit) in checks)
+ check.IsChecked = d.Coupling.Has(bit);
+ syncing = false;
+ Changed();
};
var header = new DockPanel { Margin = new Thickness(0, 0, 0, 2) };
@@ -489,7 +527,13 @@ public sealed class VehicleCards
IsChecked = d.Coupling.Has(bit),
FontSize = 12
};
- check.IsCheckedChanged += (_, _) => d.Coupling.Set(bit, check.IsChecked == true);
+ check.IsCheckedChanged += (_, _) =>
+ {
+ if (syncing) return;
+ d.Coupling.Set(bit, check.IsChecked == true);
+ Changed();
+ };
+ checks.Add((check, bit));
Grid.SetRow(check, i / 2);
Grid.SetColumn(check, i % 2);
grid.Children.Add(check);
diff --git a/StarterNG/Views/Depot/VehicleDetails.cs b/StarterNG/Views/Depot/VehicleDetails.cs
index 9c31911..294b09c 100644
--- a/StarterNG/Views/Depot/VehicleDetails.cs
+++ b/StarterNG/Views/Depot/VehicleDetails.cs
@@ -178,10 +178,6 @@ public sealed class VehicleDetails
if (unitIdx < 0)
return;
- bool isLast = unitIdx >= _consist.Count - 1;
- if (isLast)
- couplerPanel.Children.Add(PanelNote(App.Loc["CouplingLastNote"]));
-
var d = Consist.TailCar(item);
// Two columns: the tab is wide enough and the list is short enough
diff --git a/StarterNG/startercfg/lang/cz.xml b/StarterNG/startercfg/lang/cz.xml
index 99a09a8..1c15353 100644
--- a/StarterNG/startercfg/lang/cz.xml
+++ b/StarterNG/startercfg/lang/cz.xml
@@ -284,10 +284,15 @@
Rozdělit jednotku na jednotlivé vozy
Společné pro celou jednotku
Spřáhlo na konci jednotky
- Poslední vozidlo: spřáhlo 0 dává koncovky, jiný kód spojí soupravu s tím, co stojí za ní, bez koncovek. Starter zde po každé změně soupravy nastaví 0, pokud spřáhlo nenastavíte ručně; „Auto“ vždy vrátí 0.
Spojit
Spojit toto vozidlo s následujícím do jedné jednotky
- Přetáhněte pro změnu pořadí
+ Kód spřáhla: {0}
+ Vozidla nejsou spojena.
+ Spojeno: {0}
+ Konec soupravy — simulátor vyvěsí koncovky.
+ Konec soupravy se spřáhne s tím, co stojí za ní ({0}) — bez koncovek.
+ Zablokováno — při posunu se nerozpojí.
+ Klikněte pro změnu.
Přidat
Přidat vícekrát…
Kolikrát vozidlo přidat
diff --git a/StarterNG/startercfg/lang/en.xml b/StarterNG/startercfg/lang/en.xml
index 41e2a66..68b997a 100644
--- a/StarterNG/startercfg/lang/en.xml
+++ b/StarterNG/startercfg/lang/en.xml
@@ -284,10 +284,15 @@
Split the unit into individual cars
Shared by the whole unit
Coupler at the rear of the unit
- Last vehicle: coupler 0 gets the end signals; any other code couples the train to whatever stands behind it, with no end signals. The starter sets 0 here after every change to the consist, unless you set the coupler by hand; “Auto” always brings 0 back.
Join
Couple this vehicle with the next one into a single unit
- Drag to reorder
+ Coupler code: {0}
+ The vehicles are not coupled.
+ Connected: {0}
+ End of the train — the simulator puts up the end signals.
+ The end of the train couples to whatever stands behind it ({0}) — no end signals.
+ Locked — not uncoupled in shunting.
+ Click to change.
Add
Add several…
How many times to add it
diff --git a/StarterNG/startercfg/lang/hu.xml b/StarterNG/startercfg/lang/hu.xml
index bd1f31a..0cb254b 100644
--- a/StarterNG/startercfg/lang/hu.xml
+++ b/StarterNG/startercfg/lang/hu.xml
@@ -284,10 +284,15 @@
Az egység szétbontása külön kocsikra
Az egész egységre közös
Az egység végén lévő kapcsolókészülék
- Utolsó jármű: a 0-s kapcsolat adja a zárjelzőket, más kód a mögötte állóhoz kapcsolja a szerelvényt, zárjelzők nélkül. A starter minden szerelvényváltozás után 0-ra állítja, hacsak kézzel nem állítod be; az „Auto” mindig visszaállítja a 0-t.
Összekapcsolás
A jármű összekapcsolása a következővel egy egységgé
- Húzza az átrendezéshez
+ Kapcsolat kódja: {0}
+ A járművek nincsenek összekapcsolva.
+ Összekapcsolva: {0}
+ A szerelvény vége — a szimulátor kiteszi a zárjelzőket.
+ A szerelvény vége összekapcsolódik a mögötte állóval ({0}) — zárjelzők nélkül.
+ Zárolva — tolatáskor nem bontható.
+ Kattints a módosításhoz.
Hozzáadás
Hozzáadás többször…
Hányszor add hozzá a járművet
diff --git a/StarterNG/startercfg/lang/pl.xml b/StarterNG/startercfg/lang/pl.xml
index 4def396..4e6a65d 100644
--- a/StarterNG/startercfg/lang/pl.xml
+++ b/StarterNG/startercfg/lang/pl.xml
@@ -284,10 +284,15 @@
Rozdziel zespół na pojedyncze człony
Ustawienie wspólne dla całego zespołu
Sprzęg na końcu zespołu
- Ostatni pojazd: sprzęg 0 daje końcówki, a inny kod połączy skład z tym, co stoi za nim, bez końcówek. Starter ustawia tu 0 po każdej zmianie składu, chyba że ustawisz sprzęg ręcznie; „Auto” zawsze przywraca 0.
Złącz
Połącz ten pojazd z następnym w jeden zespół
- Przeciągnij, aby zmienić kolejność
+ Kod sprzęgu: {0}
+ Pojazdy nie są połączone.
+ Połączone: {0}
+ Koniec składu — symulator wystawi końcówki.
+ Koniec składu sprzęgnie się z tym, co stoi za nim ({0}) — bez końcówek.
+ Zablokowany — nie rozprzęga się przy manewrach.
+ Kliknij, aby zmienić.
Dodaj
Dodaj wiele…
Ile razy dodać pojazd
diff --git a/StarterNG/startercfg/lang/ru.xml b/StarterNG/startercfg/lang/ru.xml
index 281e700..b0c32ee 100644
--- a/StarterNG/startercfg/lang/ru.xml
+++ b/StarterNG/startercfg/lang/ru.xml
@@ -284,10 +284,15 @@
Разделить сцеп на отдельные вагоны
Общее для всего состава
Сцепка в конце состава
- Последняя единица: сцепка 0 даёт хвостовые сигналы, другой код сцепит состав с тем, что стоит за ним, без хвостовых сигналов. Стартер ставит здесь 0 после каждого изменения состава, если вы не задали сцепку вручную; «Авто» всегда возвращает 0.
Соединить
Соединить этот вагон со следующим в один состав
- Перетащите для изменения порядка
+ Код сцепки: {0}
+ Вагоны не сцеплены.
+ Соединено: {0}
+ Хвост поезда — симулятор вывесит хвостовые сигналы.
+ Хвост поезда сцепится с тем, что стоит за ним ({0}) — без хвостовых сигналов.
+ Заблокирована — не расцепляется при маневрах.
+ Нажмите, чтобы изменить.
Добавить
Добавить несколько…
Сколько раз добавить единицу