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}) — без хвостовых сигналов. + Заблокирована — не расцепляется при маневрах. + Нажмите, чтобы изменить. Добавить Добавить несколько… Сколько раз добавить единицу