From 00a62ab5688b06c09a13fef4fe51be826a9ea492 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 4 Oct 2026 18:55:16 +0200 Subject: [PATCH] Colour the depot couplers by what they connect Each coupler on the consist strip now shows its state: free, coupled, multiple-unit, permanent, and at the back whether the end signals go up. The tooltip gives the code and the connections instead of the drag hint, and the flyout edits mark the coupler as set by hand and restyle it in place. The note about 0 on the last vehicle is gone - the colour says it. --- StarterNG.Tests/CouplerLookTests.cs | 25 +++++++ StarterNG/Views/Depot/CouplerLook.cs | 95 +++++++++++++++++++++++++ StarterNG/Views/Depot/VehicleCards.cs | 68 ++++++++++++++---- StarterNG/Views/Depot/VehicleDetails.cs | 4 -- StarterNG/startercfg/lang/cz.xml | 9 ++- StarterNG/startercfg/lang/en.xml | 9 ++- StarterNG/startercfg/lang/hu.xml | 9 ++- StarterNG/startercfg/lang/pl.xml | 9 ++- StarterNG/startercfg/lang/ru.xml | 9 ++- 9 files changed, 211 insertions(+), 26 deletions(-) create mode 100644 StarterNG.Tests/CouplerLookTests.cs create mode 100644 StarterNG/Views/Depot/CouplerLook.cs 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}) — без хвостовых сигналов. + Заблокирована — не расцепляется при маневрах. + Нажмите, чтобы изменить. Добавить Добавить несколько… Сколько раз добавить единицу