Black box: the queries of the user's train worked out once a read, lights as the driver checks them

- The stations, the signals passed, the horn points, the couplings, the
  consists and the timetable progress of the user's train are kept until
  a read adds something; the views asked for them every frame, which on
  a long session with many trains took the window down. A read that adds
  nothing no longer counts as a change.
- The lights are checked against the lamps the vehicles at the ends have
  (written by the simulator from now on), as its driver checks them: Pc 1
  without the upper headlight, with the auxiliary lamps, Pc 5 with the
  rear-end signals. Only while the user drives: the autopilot's lights
  are its own.
- A train off the platform counts at both ends: its head past the stop
  point as well as its tail before the platform's end.
- A recording without the couplings keeps the timetable for any vehicle
  of the user.
- A signal passed at danger is ringed on the map only once it was.
- In the tapes window a black box opens on a click or Enter, not when
  the arrow keys go over it.
- The viewer's screenshot of a window not laid out saves nothing and
  still closes.
This commit is contained in:
maj00r
2026-11-11 00:57:57 +01:00
parent e530b95352
commit 532875fc7b
14 changed files with 254 additions and 52 deletions

View File

@@ -16,7 +16,8 @@ internal static class Screenshot
window.FollowTrain = follow;
// A few frames for the layout and the first render.
await Task.Delay(1000);
if (window.Content is Control content)
// Not laid out (a minimised window): nothing to save, and the viewer still closes.
if (window.Content is Control content && content.Bounds.Width >= 1 && content.Bounds.Height >= 1)
{
var size = new PixelSize((int)content.Bounds.Width, (int)content.Bounds.Height);
using var bitmap = new RenderTargetBitmap(size);

View File

@@ -148,7 +148,7 @@ public sealed class DefaultBlackBoxStrings : IBlackBoxStrings
["TtStopPast"] = "{0:0.0} m past",
["TtStopExact"] = "on the spot",
["TtOnPlatform"] = "along the platform",
["TtOffPlatform"] = "the tail {0:0.0} m off the platform",
["TtOffPlatform"] = "{0:0.0} m of the train off the platform",
["TtTechnical"] = "technical stop",
["TtStanding"] = "standing",
["EventStopped"] = "Stop at {0}, {1}",
@@ -347,7 +347,7 @@ public sealed class DefaultBlackBoxStrings : IBlackBoxStrings
["TtStopPast"] = "{0:0.0} m za miejscem",
["TtStopExact"] = "w punkt",
["TtOnPlatform"] = "cały przy peronie",
["TtOffPlatform"] = "koniec składu {0:0.0} m poza peronem",
["TtOffPlatform"] = "{0:0.0} m składu poza peronem",
["TtTechnical"] = "postój techniczny",
["TtStanding"] = "stoi",
["EventStopped"] = "Postój: {0}, {1}",

View File

@@ -130,19 +130,20 @@ public sealed class CabPanel : Control
double lightsX = Pad + 3 * third + 4;
context.DrawText(MapView.Text(Strings["CabLights"], Palette.DimText, 10), new Point(lightsX, top));
double faceX = lightsX;
foreach (var (key, lights, required) in new[]
foreach (var (key, lights, required, right) in new[]
{
("CabLightsHead", s.LightsHead, HeadCodes.RequiredHead(s)),
("CabLightsTail", s.LightsTail, HeadCodes.RequiredTail(s))
("CabLightsHead", s.LightsHead, HeadCodes.RequiredHead(s), HeadCodes.HeadRight(s)),
("CabLightsTail", s.LightsTail, HeadCodes.RequiredTail(s), HeadCodes.TailRight(s))
})
{
DrawFace(context, lights, new Point(faceX + 18, top + 24));
// the end, and the signal it shows: green as it is to be, red otherwise with the one it is to show
// the end, and the signal it shows: green as it is to be (with the lamps the vehicle has), red otherwise
// with the one it is to show
var shown = HeadCodes.Of(lights);
var text = required is { } want && want != shown
var text = right == false && required is { } want
? $"{HeadCodes.Name(shown)} ≠ {HeadCodes.Name(want)}"
: HeadCodes.Name(shown);
var brush = required is null ? Palette.FaintText : required == shown ? Palette.Good : Palette.Bad;
: right == true && required is { } met ? HeadCodes.Name(met) : HeadCodes.Name(shown);
var brush = right is null ? Palette.FaintText : right.Value ? Palette.Good : Palette.Bad;
var label = MapView.Text($"{Strings[key]} {text}", brush, 9);
context.DrawText(label, new Point(faceX + 18 - label.Width / 2, top + 34));
faceX += 62;

View File

@@ -456,12 +456,14 @@ public sealed class MapView : Control
var danger = new Pen(Palette.Over, 2.5);
foreach (var passing in recording.UserPassings())
{
if (passing.Time > _time)
continue;
var p = ToScreen(passing.Position.X, passing.Position.Z);
if (!Visible(p))
continue;
if (passing.AtDanger)
context.DrawEllipse(null, danger, p, 10, 10);
else if (passing.Time <= _time)
else
context.DrawEllipse(null, passed, p, 7, 7);
}
}

View File

@@ -81,7 +81,8 @@ public sealed class BlackBoxReader
}
_offset += at;
Recording.Changed();
if (read > 0)
Recording.Changed();
return read;
}
@@ -161,6 +162,9 @@ public sealed class BlackBoxReader
// What the driver went by and the lights asked for came later still.
if (r.Remaining >= 5)
sample = sample with { Movement = (Movement)r.U8(), LightHintHead = (short)r.U16(), LightHintTail = (short)r.U16() };
// The lamps the vehicles at the ends have, which a pattern is checked against, later again.
if (r.Remaining >= 4)
sample = sample with { EquippedHead = (Lights)r.U16(), EquippedTail = (Lights)r.U16() };
rec.AddCab(sample);
break;
}

View File

@@ -124,15 +124,16 @@ public sealed record DrivingSummary(
else if (sample.Driving == Driver.Human && sample.AbsSpeedKmh > Rounding)
driven += step;
// the light signals at both ends, while the train moves
if (sample.AbsSpeedKmh > 1 && HeadCodes.Right(sample) is { } right)
// the light signals at both ends, while the user drives the train and it moves: the autopilot's are its own
if (sample.Driving == Driver.Human && sample.AbsSpeedKmh > 1 &&
HeadCodes.HeadRight(sample) is { } headRight && HeadCodes.TailRight(sample) is { } tailRight)
{
lightsJudged += step;
if (!right)
if (!headRight || !tailRight)
{
lightsWrong += step;
var fault = new LightsFault(before.Time, sample.Time, HeadCodes.Of(sample.LightsHead), HeadCodes.RequiredHead(sample)!.Value,
HeadCodes.Of(sample.LightsTail), HeadCodes.RequiredTail(sample)!.Value);
!headRight, HeadCodes.Of(sample.LightsTail), HeadCodes.RequiredTail(sample)!.Value, !tailRight);
if (faults.Count > 0 && faults[^1].SameAs(fault) && fault.Start - faults[^1].End < 2)
faults[^1] = faults[^1] with { End = sample.Time };
else
@@ -186,11 +187,10 @@ public sealed record DrivingSummary(
}
/// <summary>A stretch the user's train moved with other light signals than it was to show, at its head and its tail.</summary>
public sealed record LightsFault(double Start, double End, HeadCode Head, HeadCode HeadRequired, HeadCode Tail, HeadCode TailRequired)
public sealed record LightsFault(double Start, double End, HeadCode Head, HeadCode HeadRequired, bool HeadWrong,
HeadCode Tail, HeadCode TailRequired, bool TailWrong)
{
public bool HeadWrong => Head != HeadRequired;
public bool TailWrong => Tail != TailRequired;
public bool SameAs(LightsFault other) =>
Head == other.Head && HeadRequired == other.HeadRequired && Tail == other.Tail && TailRequired == other.TailRequired;
Head == other.Head && HeadRequired == other.HeadRequired && HeadWrong == other.HeadWrong &&
Tail == other.Tail && TailRequired == other.TailRequired && TailWrong == other.TailWrong;
}

View File

@@ -70,12 +70,81 @@ public static class HeadCodes
_ => null
};
/// <summary>Whether both ends showed what they were to while the train moved; null when nothing tells it.</summary>
public static bool? Right(CabSample sample)
/// <summary>Whether the head of the train showed what it was to; null when nothing tells it.</summary>
public static bool? HeadRight(CabSample sample) => sample.Movement switch
{
if (RequiredHead(sample) is not { } head || RequiredTail(sample) is not { } tail)
return null;
return Of(sample.LightsHead) == head && Of(sample.LightsTail) == tail;
Movement.Train => Meets(sample.LightsHead, sample.EquippedHead, sample.LightHintHead, HeadCode.Pc1),
Movement.Shunting => Shows(HeadCode.Tb1, sample.LightsHead, sample.EquippedHead),
_ => null
};
/// <summary>Whether the tail of the train showed what it was to; null when nothing tells it.</summary>
public static bool? TailRight(CabSample sample) => sample.Movement switch
{
Movement.Train => Meets(sample.LightsTail, sample.EquippedTail, sample.LightHintTail, HeadCode.Pc5),
Movement.Shunting => Shows(HeadCode.Tb1, sample.LightsTail, sample.EquippedTail),
_ => null
};
/// <summary>Whether both ends showed what they were to; null when nothing tells it.</summary>
public static bool? Right(CabSample sample) =>
HeadRight(sample) is { } head && TailRight(sample) is { } tail ? head && tail : null;
private const Lights HighBeams = Lights.HighBeamLeft | Lights.HighBeamRight;
private const Lights Auxiliary = Lights.AuxiliaryLeft | Lights.AuxiliaryRight;
// the pattern asked for at an end, the usual one when none is: the driver sets one asked for as it is
private static bool Meets(Lights lit, Lights? equipped, short hint, HeadCode usual)
{
if (hint < 0)
return Shows(usual, lit, equipped);
var pattern = (Lights)hint;
if (usual == HeadCode.Pc1 && pattern == (Lights.RedMarkerLeft | Lights.HeadlightRight | Lights.HeadlightUpper))
return Shows(HeadCode.Pc2, lit, equipped);
return (lit & ~HighBeams) == (equipped is { } lamps ? pattern & lamps : pattern);
}
/// <summary>
/// Whether the lamps lit at an end show the signal, as the simulator's driver checks it against the lamps
/// the vehicle has there: a vehicle without the upper headlight shows Pc 1 with the two others, one with
/// auxiliary lamps may show them instead, and one with no lamps for it the rear-end signals. A recording
/// made before the lamps were written has the pattern alone. The high beams tell nothing.
/// </summary>
public static bool Shows(HeadCode code, Lights lit, Lights? equipped)
{
if (equipped is not { } lamps)
return Of(lit) == code;
lit &= ~HighBeams;
bool auxiliary = (lamps & Auxiliary) != 0;
switch (code)
{
case HeadCode.Pc1:
{
var pattern = lamps & (Lights.HeadlightLeft | Lights.HeadlightRight | Lights.HeadlightUpper);
var fallback = lamps & (Auxiliary | Lights.HeadlightUpper);
return (pattern != 0 && lit == pattern) || (auxiliary && lit == fallback) ||
(pattern == 0 && fallback == 0 && lit == Lights.RearEndSignals);
}
case HeadCode.Pc2:
{
var pattern = lamps & (Lights.RedMarkerLeft | Lights.HeadlightRight | Lights.HeadlightUpper);
var fallback = lamps & (Lights.RedMarkerLeft | Lights.AuxiliaryRight | Lights.HeadlightUpper);
return lit == pattern || (auxiliary && lit == fallback);
}
case HeadCode.Pc5:
{
var pattern = lamps & (Lights.RedMarkerLeft | Lights.RedMarkerRight);
var fallback = lamps & Lights.RearEndSignals;
return (pattern != 0 && lit == pattern) || (fallback != 0 && lit == fallback);
}
case HeadCode.Tb1:
// one headlight, of those there are
return lit == (lamps & Lights.HeadlightLeft) || lit == (lamps & Lights.HeadlightRight);
case HeadCode.Off:
return lit == Lights.None;
default:
return false;
}
}
/// <summary>The name of a signal as the rules give it.</summary>

View File

@@ -91,7 +91,27 @@ public sealed class Recording
// ----- filled by the reader -----
internal void Changed() => ++Revision;
internal void Changed()
{
++Revision;
_memo.Clear();
_consists.Clear();
}
// What the questions about the user's train worked out, kept until a read adds something: the views ask them every frame.
private readonly Dictionary<object, object> _memo = new();
private readonly Dictionary<(int Vehicle, int Coupling), IReadOnlySet<int>> _consists = new();
private static readonly object UserStationsKey = new(), UserPassingsKey = new(), UserHornChecksKey = new(), UserCouplingsKey = new();
/// <summary>What make gives, worked out once until the recording changes.</summary>
internal T Memo<T>(object key, Func<T> make) where T : class
{
if (_memo.TryGetValue(key, out var kept))
return (T)kept;
var made = make();
_memo[key] = made;
return made;
}
private void Saw(double time) => _lastTime = Math.Max(_lastTime, time);
@@ -371,8 +391,8 @@ public sealed class Recording
public IReadOnlyList<TimeSpanRange> AutopilotSpans() => CabSpans(s => s.Driving == Driver.Ai);
/// <summary>The signals the user's train passed: those passed by the vehicle the samples are of at the time.</summary>
public IEnumerable<SignalPassing> UserPassings() =>
_passings.Where(p => IsUserTrain(p.Time + 0.5, p.Vehicle, p.Position, Math.Max(0, p.Overrun) + 250));
public IReadOnlyList<SignalPassing> UserPassings() =>
Memo(UserPassingsKey, () => _passings.Where(p => IsUserTrain(p.Time + 0.5, p.Vehicle, p.Position, Math.Max(0, p.Overrun) + 250)).ToList());
/// <summary>
/// Whether a vehicle was of the user's train at t. The records of a train give the
@@ -393,12 +413,22 @@ public sealed class Recording
/// <summary>The vehicles coupled into one train with the one given at t, itself included.</summary>
public IReadOnlySet<int> ConsistAt(int vehicle, double t)
{
// What each coupler held at t: the couplings in the order written, each telling both ends.
// The trains change only at a coupling: the consist is the same until the next one.
int last = LastAtOrBefore(_couplings, t, c => c.Time);
if (_consists.TryGetValue((vehicle, last), out var kept))
return kept;
var consist = Consist(vehicle, last);
_consists[(vehicle, last)] = consist;
return consist;
}
private HashSet<int> Consist(int vehicle, int last)
{
// What each coupler held after the coupling given: the couplings in the order written, each telling both ends.
var held = new Dictionary<(int Vehicle, int Side), (int Other, int OtherSide)>();
foreach (var change in _couplings)
for (int i = 0; i <= last; i++)
{
if (change.Time > t)
break;
var change = _couplings[i];
if (held.Remove((change.Vehicle, change.Side), out var before))
held.Remove((before.Other, before.OtherSide));
if (change.Other != 0 && (change.Flags & CouplingFlags.Coupler) != 0)
@@ -432,10 +462,11 @@ public sealed class Recording
_couplings.Where(c => c.Time > 0.01 && (c.Coupled || c.Uncoupled || c.Flags != c.PreviousFlags));
/// <summary>The couplings the user's train took part in, before or after them.</summary>
public IEnumerable<CouplingChange> UserCouplings() =>
CouplingEvents().Where(c => CabAt(c.Time) is { } s && c.Time - s.Time <= 1.5 &&
(ConsistAt(s.Vehicle, c.Time - 0.01).Contains(c.Vehicle) ||
ConsistAt(s.Vehicle, c.Time + 0.01).Contains(c.Vehicle)));
public IReadOnlyList<CouplingChange> UserCouplings() =>
Memo(UserCouplingsKey, () => CouplingEvents().Where(c => CabAt(c.Time) is { } s && c.Time - s.Time <= 1.5 &&
(ConsistAt(s.Vehicle, c.Time - 0.01).Contains(c.Vehicle) ||
ConsistAt(s.Vehicle, c.Time + 0.01).Contains(c.Vehicle)))
.ToList());
/// <summary>
/// The timetable of the train the user was in at t: the last one written by then, else the first; none when the
@@ -444,18 +475,21 @@ public sealed class Recording
public Timetable? TimetableAt(double t)
{
var timetable = LastAtOrBefore(_timetables, t, x => x.Time) is var i and >= 0 ? _timetables[i] : _timetables.FirstOrDefault();
if (timetable is null || CabAt(t) is not { } sample || sample.Vehicle == timetable.Vehicle)
// A recording made before the couplings were written can't tell the trains apart: the timetable stays.
if (timetable is null || CabAt(t) is not { } sample || sample.Vehicle == timetable.Vehicle || _couplings.Count == 0)
return timetable;
// another vehicle of the same train (the other cab, a car of it) keeps the timetable
return ConsistAt(sample.Vehicle, t).Contains(timetable.Vehicle) ? timetable : null;
}
/// <summary>The stations of the timetable of the user's train, as it came to them.</summary>
public IEnumerable<StationEvent> UserStations() =>
_stations.Where(s => IsUserTrain(s.Time, s.Vehicle, s.Position, Math.Max(s.TrainLength, 50) + 150));
public IReadOnlyList<StationEvent> UserStations() =>
Memo(UserStationsKey, () => _stations.Where(s => IsUserTrain(s.Time, s.Vehicle, s.Position, Math.Max(s.TrainLength, 50) + 150)).ToList());
/// <summary>The horn points the user's train passed, and whether the horn sounded there.</summary>
public IEnumerable<HornCheck> UserHornChecks()
public IReadOnlyList<HornCheck> UserHornChecks() => Memo(UserHornChecksKey, () => HornChecks().ToList());
private IEnumerable<HornCheck> HornChecks()
{
foreach (var request in _hornRequests)
{

View File

@@ -240,13 +240,16 @@ public sealed record StationEvent(double Time, int Vehicle, StationKind Kind, in
/// Whether the whole train stood along the platform, when its length is known: as the driver takes it, the length of
/// the trains a W32 is for is the room it has there too.
/// </summary>
public bool? OnPlatform => Platform != 0 ? Distance >= -2 && Distance + TrainLength <= PlatformLength + 2 : null;
public bool? OnPlatform => Platform != 0 ? OffPlatform <= PlatformTolerance : null;
/// <summary>How far off the platform a train may stand and still count as along it [m].</summary>
public const double PlatformTolerance = 2;
/// <summary>The length of the platform [m], without the digit of its side.</summary>
public double PlatformLength => Platforms.Length(Platform);
/// <summary>How far the tail stood off the platform [m], 0 along it.</summary>
public double OffPlatform => Platform != 0 ? Math.Max(0, Distance + TrainLength - PlatformLength) : 0;
/// <summary>How much of the train stood off the platform [m]: the head past the stop point, the tail before the platform's end; 0 along it.</summary>
public double OffPlatform => Platform != 0 ? Math.Max(0, -Distance) + Math.Max(0, Distance + TrainLength - PlatformLength) : 0;
}
public enum CameraMode : byte
@@ -315,7 +318,8 @@ public readonly record struct CabSample(double Time, int Vehicle, Position Posit
float MainReservoir, float Voltage, float EngineRpm,
Lights LightsHead, Lights LightsTail, sbyte CabEnd = 0,
SecurityFlags Security = SecurityFlags.None, float LateralAcceleration = 0,
Movement Movement = Movement.Unknown, short LightHintHead = -1, short LightHintTail = -1)
Movement Movement = Movement.Unknown, short LightHintHead = -1, short LightHintTail = -1,
Lights? EquippedHead = null, Lights? EquippedTail = null)
{
public bool Has(CabDevices device) => (Devices & device) == device;
public bool Has(SecurityFlags flag) => (Security & flag) == flag;

View File

@@ -66,7 +66,10 @@ public sealed record StationProgress(
recording.TimetableAt(t) is { } timetable ? Of(recording, timetable) : Array.Empty<StationProgress>();
/// <summary>The rows of a timetable of the recording, each with what the train did there.</summary>
public static IReadOnlyList<StationProgress> Of(Recording recording, Timetable timetable)
public static IReadOnlyList<StationProgress> Of(Recording recording, Timetable timetable) =>
recording.Memo((typeof(StationProgress), timetable), () => Work(recording, timetable));
private static IReadOnlyList<StationProgress> Work(Recording recording, Timetable timetable)
{
// The records of this train: from its timetable on, until another one is written. A train given its next
// timetable may come to the first station of it at once, and a recording made before the simulator wrote the

View File

@@ -108,7 +108,7 @@ internal sealed class BlackBoxFile
float target = 40, float nextLimit = -1, float nextDistance = -1, float brakeCylinder = 0,
uint devices = 0x0007, bool extended = true, sbyte cabEnd = 1, byte security = 0,
float lateral = 0.2f, ushort lightsHead = 0x15, ushort lightsTail = 0x22, byte? movement = null,
short hintHead = -1, short hintTail = -1) =>
short hintHead = -1, short hintTail = -1, ushort? equippedHead = null, ushort? equippedTail = null) =>
Record(0x20, w =>
{
w.Write(time);
@@ -154,6 +154,12 @@ internal sealed class BlackBoxFile
w.Write(kind);
w.Write(hintHead);
w.Write(hintTail);
// the lamps the vehicles at the ends have: a recording made before them ends above
if (equippedHead is { } head)
{
w.Write(head);
w.Write(equippedTail ?? head);
}
}
}
});

View File

@@ -68,6 +68,62 @@ public class HeadCodeTests
Assert.False(fault.TailWrong);
}
private static Recording DriveWith(ushort head, ushort tail, ushort equippedHead, ushort equippedTail, byte movement = 1, byte driving = 1)
{
var f = new BlackBoxFile().Info();
for (double t = 0; t < 30; t += 0.5)
f.Cab(t, 1, 1000 + t * 10, 40, driving: driving, lightsHead: head, lightsTail: tail, movement: movement,
equippedHead: equippedHead, equippedTail: equippedTail);
return BlackBoxReader.Read(f.End(30).Stream());
}
[Fact]
public void The_lamps_the_vehicles_have_are_read()
{
var sample = DriveWith(Pc1, Pc5, 0x1B5, 0x22).Cab[0];
Assert.Equal((Lights)0x1B5, sample.EquippedHead);
Assert.Equal((Lights)0x22, sample.EquippedTail);
}
[Theory]
// without the upper headlight Pc 1 is the two others
[InlineData(0x11, 0x11)]
// with auxiliary lamps, those and the upper headlight
[InlineData(0x184, 0x1B5)]
// a vehicle with no lamps for it: the rear-end signals
[InlineData(0x40, 0x40)]
public void Pc1_is_what_the_vehicle_can_show_of_it(ushort lit, ushort equipped)
{
var summary = DriveWith(lit, Pc5, equipped, Pc5).Summarize();
Assert.True(summary.LightsJudgedSeconds > 25);
Assert.Empty(summary.LightsFaults);
}
[Fact]
public void A_tail_with_the_rear_end_signals_only_shows_Pc5_with_them()
{
Assert.Empty(DriveWith(Pc1, 0x40, Pc1, 0x40).Summarize().LightsFaults);
var fault = Assert.Single(DriveWith(Pc1, 0x00, Pc1, 0x40).Summarize().LightsFaults);
Assert.False(fault.HeadWrong);
Assert.True(fault.TailWrong);
}
[Fact]
public void A_vehicle_with_all_the_lamps_is_to_light_all_three_for_Pc1()
{
var fault = Assert.Single(DriveWith(0x11, Pc5, 0x35, Pc5).Summarize().LightsFaults);
Assert.True(fault.HeadWrong);
Assert.Equal(HeadCode.Pc1, fault.HeadRequired);
}
[Fact]
public void The_lights_the_autopilot_sets_are_not_the_users()
{
var summary = DriveWith(Tb1, Tb1, Pc1, Pc5, driving: 2).Summarize();
Assert.Equal(0, summary.LightsJudgedSeconds);
Assert.Empty(summary.LightsFaults);
}
[Fact]
public void An_older_recording_tells_nothing_of_the_signals()
{

View File

@@ -31,6 +31,22 @@ public class PlatformTests
Assert.Equal(201, stop.PlatformLength);
}
[Fact]
public void A_train_whose_head_stands_past_the_stop_point_is_off_the_platform_by_that_much()
{
var stop = Stop(-5f, 129.8f, 201);
Assert.False(stop.OnPlatform);
Assert.Equal(5, stop.OffPlatform, 1);
}
[Fact]
public void A_metre_off_still_counts_as_along_the_platform()
{
var stop = Stop(-1f, 129.8f, 201);
Assert.True(stop.OnPlatform);
Assert.Equal(1, stop.OffPlatform, 1);
}
[Fact]
public void A_train_within_the_platform_is_along_it()
{

View File

@@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.Imaging;
@@ -105,14 +106,19 @@ public sealed class HaslerTapeWindow : Window
ToolTip.SetTip(_startClock, App.Loc["HaslerStartClockTip"]);
ScrollViewer.SetHorizontalScrollBarVisibility(_list, ScrollBarVisibility.Disabled);
_list.KeyDown += async (_, e) =>
{
if (e.Key == Key.Enter && _list.SelectedItem is TreeViewItem { Tag: BlackBoxRecording box })
{
e.Handled = true;
await OpenBlackBoxAsync(box);
}
};
_list.SelectionChanged += async (_, _) =>
{
// A black box opens in a window of its own; the tape shown stays.
if (_list.SelectedItem is TreeViewItem { Tag: BlackBoxRecording box })
{
await OpenBlackBoxAsync(box);
// A black box opens in a window of its own on a click or Enter, not when the arrows go over it; the tape shown stays.
if (_list.SelectedItem is TreeViewItem { Tag: BlackBoxRecording })
return;
}
// A session stands for its first tape, the player's own if it has one.
if (_list.SelectedItem is TreeViewItem { Tag: TapeSession } session)
session.IsExpanded = true;