Walk the QTC window the way N1MM walks it
Four changes to the QTC window, three of them N1MM's behaviour and one an addition. Tab, enter and space all move to the next box the operator types in, and shift and tab move back. The walk covers those boxes and nothing else. N1MM handles the three keys in every box itself — `QTCLinesA_KeyDown` and its two siblings, with `PreviewKeyDown` claiming Tab — rather than leaving them to the tab order, so the cursor never lands on a switched-off line or on a button beside one. Tab was going to Avalonia's default walk, which covered every control in the window. The header now says how many lines take traffic. `QTCHeader_LostFocus` reads the count after the last `/` and hands it to `ColorizeCWRXFields`, which enables that many lines and switches off the rest, capped by how many of the station's ten are still free. All ten were live whatever the header said. Nothing already typed is cleared, which N1MM does under `DontErase`. A time typed short is filled in from the line above as the cursor leaves the box: `52` under `1148` reads as `1152`. `QTCLinesA_LostFocus` takes as many characters off the front of the line above as the typed part is missing, and drops a colon whatever the length. `QtcTraffic.FillTime` is that rule. The addition: while the cursor is in a QTC callsign box the check window answers for that call rather than the entry window's, so a call taken down in traffic is checked the same way one being worked is. `CheckWindowSources` takes the call from `Elsewhere` when the QTC window has set it. Checked by driving the window: header `4/5` left five lines live and read "1 of 5", tab walked header to time to call to serial to the next line's time, shift and tab walked back, `52` on the second line became `1152`, and the check window listed W1AW while `W1A` was typed in a QTC line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
This commit is contained in:
@@ -1093,7 +1093,8 @@ public sealed partial class EntryWindow : Window
|
||||
Status("nothing left to report to " + station.Text);
|
||||
return;
|
||||
}
|
||||
QtcWindow window = new(session, Logging, station, direction, text => SendTextAsync(text));
|
||||
QtcWindow window = new(
|
||||
session, Logging, station, direction, text => SendTextAsync(text), Refresh);
|
||||
bool saved = await window.ShowDialog<bool>(this);
|
||||
Logging.Wipe();
|
||||
SyncBoxes();
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<Grid Name="Lines" Margin="0,6,0,0" />
|
||||
<TextBlock Name="KeysText" FontSize="11" Opacity="0.7" Margin="0,6,0,0" />
|
||||
<TextBlock FontSize="11" Opacity="0.7" TextWrapping="Wrap" MaxWidth="420"
|
||||
Text="Enter, Tab move forward — space moves within a QTC line. Green = saved, red = not filled, yellow = format error." />
|
||||
Text="Tab, enter and space move to the next box, shift and tab back. Green = saved, red = not filled, yellow = format error." />
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="6" Margin="0,6,0,0">
|
||||
<TextBlock Name="StatusText" VerticalAlignment="Center" FontSize="11" Opacity="0.7" />
|
||||
<Button Name="ClearButton" Content="Clear" Click="OnClear" />
|
||||
|
||||
@@ -36,8 +36,15 @@ public sealed partial class QtcWindow : Window
|
||||
private readonly QtcTraffic traffic;
|
||||
private readonly Callsign station;
|
||||
private readonly Func<string, Task> send;
|
||||
private readonly Action refresh;
|
||||
private bool isSending;
|
||||
|
||||
/// How many lines are live. Receiving, the header says: `4/5` is series 4
|
||||
/// with five QTCs in it, so five lines are enabled and the rest are not.
|
||||
/// It is capped by how many of the station's ten are still free, which is
|
||||
/// what N1MM's `ColorizeCWRXFields` does with it.
|
||||
private int activeLines;
|
||||
|
||||
/// The line last read out, which is the one the number keys repeat a field
|
||||
/// of.
|
||||
private int lastSent;
|
||||
@@ -51,12 +58,14 @@ public sealed partial class QtcWindow : Window
|
||||
OperatingPosition position,
|
||||
Callsign station,
|
||||
QtcDirection direction,
|
||||
Func<string, Task> send)
|
||||
Func<string, Task> send,
|
||||
Action refresh)
|
||||
{
|
||||
this.session = session;
|
||||
this.position = position;
|
||||
this.station = station;
|
||||
this.send = send;
|
||||
this.refresh = refresh;
|
||||
isSending = direction == QtcDirection.Send;
|
||||
traffic = new QtcTraffic(position);
|
||||
InitializeComponent();
|
||||
@@ -67,6 +76,8 @@ public sealed partial class QtcWindow : Window
|
||||
SendButton.IsCheckedChanged += (_, _) => SetDirection(SendButton.IsChecked == true);
|
||||
Reset();
|
||||
AddHandler(KeyDownEvent, OnWindowKeyDown, RoutingStrategies.Tunnel);
|
||||
HeaderBox.LostFocus += (_, _) => ApplyHeaderLimit();
|
||||
Closed += (_, _) => ShowCall(null);
|
||||
// focus set before the window is on screen does not stick
|
||||
Opened += (_, _) => FirstBox().Focus();
|
||||
}
|
||||
@@ -187,12 +198,48 @@ public sealed partial class QtcWindow : Window
|
||||
IsReadOnly = isSending,
|
||||
};
|
||||
box.TextChanged += (_, _) => Paint();
|
||||
if (!isSending && column == 0)
|
||||
{
|
||||
int at = row;
|
||||
box.LostFocus += (_, _) => FillTime(at);
|
||||
}
|
||||
if (!isSending && column == 1)
|
||||
{
|
||||
box.TextChanged += (_, _) => ShowCall(box.Text ?? "");
|
||||
box.GotFocus += (_, _) => ShowCall(box.Text ?? "");
|
||||
box.LostFocus += (_, _) => ShowCall(null);
|
||||
}
|
||||
Grid.SetRow(box, row);
|
||||
Grid.SetColumn(box, column);
|
||||
Lines.Children.Add(box);
|
||||
return box;
|
||||
}
|
||||
|
||||
/// A time typed short is filled in from the line above as the cursor
|
||||
/// leaves the box, which is where N1MM does it.
|
||||
private void FillTime(int at)
|
||||
{
|
||||
string above = at > 0 ? rows[at - 1].Time.Text ?? "" : "";
|
||||
string filled = QtcTraffic.FillTime(rows[at].Time.Text ?? "", above);
|
||||
if (filled != (rows[at].Time.Text ?? ""))
|
||||
{
|
||||
rows[at].Time.Text = filled;
|
||||
}
|
||||
}
|
||||
|
||||
/// Points the check window at the callsign being typed here rather than
|
||||
/// the one in the entry window, and back again when the cursor leaves.
|
||||
/// N1MM leaves the check window on the entry window's call.
|
||||
private void ShowCall(string? call)
|
||||
{
|
||||
if (session.Check is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
session.Check.Elsewhere = call;
|
||||
refresh();
|
||||
}
|
||||
|
||||
/// Sending, the header and the lines come from the log. Receiving, both are
|
||||
/// empty and the operator fills them in.
|
||||
private void Fill()
|
||||
@@ -201,14 +248,17 @@ public sealed partial class QtcWindow : Window
|
||||
if (!isSending)
|
||||
{
|
||||
HeaderBox.Text = "";
|
||||
foreach (Row row in rows)
|
||||
// until a header says otherwise, every line the station has left
|
||||
activeLines = Math.Min(traffic.Remaining(station), rows.Count);
|
||||
for (int at = 0; at < rows.Count; at++)
|
||||
{
|
||||
row.SetEnabled(true);
|
||||
rows[at].SetEnabled(at < activeLines);
|
||||
}
|
||||
Paint();
|
||||
return;
|
||||
}
|
||||
ready.AddRange(traffic.ToSend(station, session.Settings.QtcLinesPerSeries));
|
||||
activeLines = ready.Count;
|
||||
HeaderBox.Text = ready.Count == 0
|
||||
? ""
|
||||
: IsRtty
|
||||
@@ -229,6 +279,27 @@ public sealed partial class QtcWindow : Window
|
||||
Paint();
|
||||
}
|
||||
|
||||
/// Narrows the live lines to what the header asks for. `4/5` is series 4
|
||||
/// with five QTCs in it, so five lines take traffic and the other five are
|
||||
/// switched off. A station with fewer than five of its ten left gets that
|
||||
/// many, which is the cap N1MM applies in `ColorizeCWRXFields`. Nothing
|
||||
/// already typed is cleared.
|
||||
private void ApplyHeaderLimit()
|
||||
{
|
||||
if (isSending)
|
||||
{
|
||||
return;
|
||||
}
|
||||
int free = Math.Min(traffic.Remaining(station), rows.Count);
|
||||
int wanted = QtcTraffic.ReadHeader(HeaderBox.Text ?? "") is { } header ? header.Count : free;
|
||||
activeLines = Math.Clamp(Math.Min(wanted, free), 0, rows.Count);
|
||||
for (int at = 0; at < rows.Count; at++)
|
||||
{
|
||||
rows[at].SetEnabled(at < activeLines);
|
||||
}
|
||||
Paint();
|
||||
}
|
||||
|
||||
/// Green when the line is ready to save, yellow when it is filled in but
|
||||
/// cannot be read, red while it is still empty.
|
||||
private void Paint()
|
||||
@@ -243,8 +314,9 @@ public sealed partial class QtcWindow : Window
|
||||
Row row = rows[at];
|
||||
bool empty = row.IsEmpty;
|
||||
WaeQtc? line = empty || header is null ? null : ReadLine(at, header.Value);
|
||||
IBrush colour = line is not null ? Saved : empty ? Unsaved : Malformed;
|
||||
row.Paint(colour);
|
||||
row.Paint(at >= activeLines
|
||||
? null
|
||||
: line is not null ? Saved : empty ? Unsaved : Malformed);
|
||||
if (line is not null)
|
||||
{
|
||||
good++;
|
||||
@@ -263,8 +335,10 @@ public sealed partial class QtcWindow : Window
|
||||
rows[at].Call.Text ?? "",
|
||||
rows[at].Number.Text ?? "");
|
||||
|
||||
/// Enter and Tab move forward through the boxes; space moves within a line,
|
||||
/// which is how an operator walks a QTC while listening.
|
||||
/// Tab, enter and space all move to the next box the operator types in,
|
||||
/// and shift and tab move back. N1MM handles the three keys itself in
|
||||
/// every box rather than leaving them to the tab order, so the walk never
|
||||
/// lands on a switched-off line or on a button beside one.
|
||||
private void OnWindowKeyDown(object? sender, KeyEventArgs e)
|
||||
{
|
||||
switch (e.Key)
|
||||
@@ -273,6 +347,10 @@ public sealed partial class QtcWindow : Window
|
||||
e.Handled = true;
|
||||
Close(false);
|
||||
break;
|
||||
case Key.Tab:
|
||||
e.Handled = true;
|
||||
Move(e.KeyModifiers.HasFlag(KeyModifiers.Shift) ? -1 : 1);
|
||||
break;
|
||||
case >= Key.D1 and <= Key.D4 when IsCw && isSending:
|
||||
case >= Key.NumPad1 and <= Key.NumPad4 when IsCw && isSending:
|
||||
e.Handled = true;
|
||||
@@ -284,21 +362,32 @@ public sealed partial class QtcWindow : Window
|
||||
AskFieldAgain(FieldOf(e.Key));
|
||||
break;
|
||||
case Key.Enter when e.Source is TextBox:
|
||||
e.Handled = true;
|
||||
MoveOn();
|
||||
break;
|
||||
case Key.Space when e.Source is TextBox:
|
||||
e.Handled = true;
|
||||
MoveOn();
|
||||
Move(1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void MoveOn()
|
||||
/// The boxes and buttons the operator works through, in order and without
|
||||
/// the ones that are switched off: the header, then each live line, and
|
||||
/// Close at the end. Reading traffic out there is nothing to type, so the
|
||||
/// walk is over the send buttons instead.
|
||||
private List<Control> WalkOrder()
|
||||
{
|
||||
List<Control> order = [HeaderBox];
|
||||
foreach (Row row in rows)
|
||||
List<Control> order = [];
|
||||
if (!isSending)
|
||||
{
|
||||
order.Add(HeaderBox);
|
||||
}
|
||||
for (int at = 0; at < rows.Count && at < activeLines; at++)
|
||||
{
|
||||
Row row = rows[at];
|
||||
if (isSending)
|
||||
{
|
||||
order.Add(row.Again);
|
||||
continue;
|
||||
}
|
||||
order.Add(row.Time);
|
||||
order.Add(row.Call);
|
||||
order.Add(row.Number);
|
||||
@@ -307,12 +396,22 @@ public sealed partial class QtcWindow : Window
|
||||
order.Add(row.Confirm);
|
||||
}
|
||||
}
|
||||
order.Add(CloseButton);
|
||||
return order;
|
||||
}
|
||||
|
||||
/// One step along the walk. Past the last box the cursor lands on Close,
|
||||
/// which is where N1MM leaves it; before the first it stays put.
|
||||
private void Move(int by)
|
||||
{
|
||||
List<Control> order = WalkOrder();
|
||||
int at = order.FindIndex(b => b.IsFocused);
|
||||
if (at < 0)
|
||||
{
|
||||
order[0].Focus();
|
||||
return;
|
||||
}
|
||||
for (int next = at + 1; next < order.Count; next++)
|
||||
for (int next = at + by; next >= 0 && next < order.Count; next += by)
|
||||
{
|
||||
if (order[next].IsEffectivelyEnabled)
|
||||
{
|
||||
@@ -320,7 +419,10 @@ public sealed partial class QtcWindow : Window
|
||||
return;
|
||||
}
|
||||
}
|
||||
CloseButton.Focus();
|
||||
if (by > 0)
|
||||
{
|
||||
CloseButton.Focus();
|
||||
}
|
||||
}
|
||||
|
||||
/// Reading traffic out, this asks the other station whether it is ready;
|
||||
@@ -587,11 +689,21 @@ public sealed partial class QtcWindow : Window
|
||||
&& (Call.Text ?? "").Trim().Length == 0
|
||||
&& (Number.Text ?? "").Trim().Length == 0;
|
||||
|
||||
public void Paint(IBrush colour)
|
||||
/// A null colour is a line that is switched off, which keeps the
|
||||
/// window's own background rather than any of the three.
|
||||
public void Paint(IBrush? colour)
|
||||
{
|
||||
Time.Background = colour;
|
||||
Call.Background = colour;
|
||||
Number.Background = colour;
|
||||
foreach (TextBox box in new[] { Time, Call, Number })
|
||||
{
|
||||
if (colour is null)
|
||||
{
|
||||
box.ClearValue(BackgroundProperty);
|
||||
}
|
||||
else
|
||||
{
|
||||
box.Background = colour;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
|
||||
Reference in New Issue
Block a user