Take the rest of what N1MM's QTC window does on CW
Reading the source again after this was tried turned up several things it had
wrong or left out.
The TU message went out only when reading a series out. N1MM sends it whichever
way the traffic went, and lets it name the series: {QTC} in it stands for the
header, so TU {QTC} 73 goes out as TU QTC 3/10 73.
The RX Ready button was hidden while sending and sent nothing on CW. It is
N1MM's QRV button: reading out it says R U QRV and keys QRV?, taking down it
says QRV and keys QRV.
The fields were asked for again with shift and Enter, which was made up. N1MM
uses the number keys, and writes them under the lines: 1, 2, 3 and 4 send the
time, call, serial or header of the last line again while reading out, and
shift with 1, 2 or 3 asks for that field while taking down.
The buttons now carry N1MM's names for the direction the traffic is going —
R U QRV, Snd Hdr, Snd1 to Snd10, Exit reading out; QRV, Hdr Agn, Agn1 to Agn10,
Exit taking down — and the Cfm column and Clear are hidden while reading out,
as N1MM hides them. The separate Send Hdr button is gone: N1MM has one button
there that sends the header one way and asks for it the other.
The log window gained N1MM's QTC? column for a contest that carries QTC
traffic. It holds the series, so a QTC row can be told from a contact; before
this a QTC row read as an ordinary one with odd values in the report columns.
Driven under Xvfb against a fake cwdaemon. Reading out: QRV?, QTC 3/2,
2319 DL7GGG 03, then 2319 and QTC 3/2 from the number keys, and
TU QTC 3/2 73 on Exit. Taking down: QRV, AGN TIME, AGN CALL.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
This commit is contained in:
@@ -18,7 +18,6 @@
|
||||
<TextBlock Text="QTC Header :" FontSize="11" Opacity="0.7" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<TextBox Name="HeaderBox" Width="170" />
|
||||
<Button Name="HeaderSendButton" Content="Send Hdr" Click="OnHeaderSend" />
|
||||
<Button Name="ReadyButton" Content="RX Ready" Click="OnReady" />
|
||||
<Button Name="HeaderAgainButton" Content="Hdr Agn" Click="OnHeaderAgain" />
|
||||
<Button Name="HeaderCfmButton" Content="Cfm" Click="OnHeaderConfirm" />
|
||||
@@ -29,7 +28,7 @@
|
||||
Text="Enter, Tab move forward — space moves within a QTC line. 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 Content="Clear" Click="OnClear" />
|
||||
<Button Name="ClearButton" Content="Clear" Click="OnClear" />
|
||||
<Button Name="CloseButton" Content="Close" Click="OnClose" IsDefault="True" />
|
||||
<Button Content="Cancel" Click="OnCancel" />
|
||||
</StackPanel>
|
||||
|
||||
@@ -35,6 +35,10 @@ public sealed partial class QtcWindow : Window
|
||||
private readonly Callsign station;
|
||||
private readonly Func<string, Task> send;
|
||||
private bool isSending;
|
||||
|
||||
/// The line last read out, which is the one the number keys repeat a field
|
||||
/// of.
|
||||
private int lastSent;
|
||||
private readonly List<Row> rows = [];
|
||||
private readonly List<WaeQtc> ready = [];
|
||||
|
||||
@@ -72,6 +76,21 @@ public sealed partial class QtcWindow : Window
|
||||
: session.Settings.QtcSkipReady ? HeaderBox
|
||||
: ReadyButton;
|
||||
|
||||
/// N1MM writes the number keys under the lines. They only do anything on
|
||||
/// CW, so the line says something else on the other modes.
|
||||
private string KeysLine()
|
||||
{
|
||||
if (!IsCw)
|
||||
{
|
||||
return isSending
|
||||
? $"{traffic.Remaining(station)} of the ten QTCs for {station.Text} are still free"
|
||||
: "Type the header, then a line per QTC: time, callsign, serial number.";
|
||||
}
|
||||
return isSending
|
||||
? "1 = time 2 = call 3 = serial 4 = header — sends that field of the line again"
|
||||
: "Shift 1 = ask time Shift 2 = ask call Shift 3 = ask serial";
|
||||
}
|
||||
|
||||
/// Traffic goes out on CW alone. The contest says which mode it is, not
|
||||
/// what the radio happens to be on, which is how N1MM decides too.
|
||||
private bool IsCw => position.Contest.Modes is [ModeCategory.Cw];
|
||||
@@ -91,13 +110,16 @@ public sealed partial class QtcWindow : Window
|
||||
private void Reset()
|
||||
{
|
||||
Title = isSending ? $"Send QTC · {station.Text}" : $"Receive QTC · {station.Text}";
|
||||
KeysText.Text = isSending
|
||||
? $"{traffic.Remaining(station)} of the ten QTCs for {station.Text} are still free"
|
||||
: "Type the header, then a line per QTC: time, callsign, serial number.";
|
||||
ReadyButton.IsVisible = !isSending;
|
||||
HeaderAgainButton.IsVisible = !isSending;
|
||||
KeysText.Text = KeysLine();
|
||||
// N1MM's labels: the buttons say what they do in the direction the
|
||||
// traffic is going, and the ones with nothing to do are hidden
|
||||
ReadyButton.IsVisible = IsCw || !isSending;
|
||||
ReadyButton.Content = !IsCw ? "RX Ready" : isSending ? "R U QRV" : "QRV";
|
||||
HeaderAgainButton.Content = isSending && IsCw ? "Snd Hdr" : "Hdr Agn";
|
||||
HeaderAgainButton.IsVisible = !isSending || IsCw;
|
||||
HeaderCfmButton.IsVisible = !isSending;
|
||||
HeaderSendButton.IsVisible = isSending && IsCw;
|
||||
ClearButton.IsVisible = !isSending;
|
||||
CloseButton.Content = IsCw ? "Exit" : "Close";
|
||||
HeaderBox.IsReadOnly = isSending;
|
||||
BuildRows();
|
||||
Fill();
|
||||
@@ -122,15 +144,16 @@ public sealed partial class QtcWindow : Window
|
||||
TextBox number = Box(at, 2);
|
||||
Button again = new()
|
||||
{
|
||||
Content = isSending && IsCw ? $"Send{at + 1}" : $"Agn{at + 1}",
|
||||
Content = isSending && IsCw ? $"Snd{at + 1}" : $"Agn{at + 1}",
|
||||
Margin = new Avalonia.Thickness(2, 1, 2, 1),
|
||||
};
|
||||
Button confirm = new() { Content = $"Cfm{at + 1}", Margin = new Avalonia.Thickness(2, 1, 2, 1) };
|
||||
int line = at;
|
||||
again.Click += (_, _) => OnLineAgain(line);
|
||||
confirm.Click += (_, _) => OnLineConfirm(line);
|
||||
// nothing to confirm when a RTTY station is reading its own log out
|
||||
confirm.IsVisible = !(isSending && position.Contest.Modes.Contains(ModeCategory.Digital));
|
||||
// nothing to confirm while reading our own log out, which is how
|
||||
// N1MM hides the column
|
||||
confirm.IsVisible = !isSending;
|
||||
Grid.SetRow(again, at);
|
||||
Grid.SetColumn(again, 3);
|
||||
Grid.SetRow(confirm, at);
|
||||
@@ -238,13 +261,15 @@ public sealed partial class QtcWindow : Window
|
||||
e.Handled = true;
|
||||
Close(false);
|
||||
break;
|
||||
case Key.Enter when e.Source is TextBox box
|
||||
&& !isSending
|
||||
&& IsCw
|
||||
&& e.KeyModifiers.HasFlag(KeyModifiers.Shift)
|
||||
&& box != HeaderBox:
|
||||
case >= Key.D1 and <= Key.D4 when IsCw && isSending:
|
||||
case >= Key.NumPad1 and <= Key.NumPad4 when IsCw && isSending:
|
||||
e.Handled = true;
|
||||
SendFieldAgain(box);
|
||||
SendField(FieldOf(e.Key));
|
||||
break;
|
||||
case >= Key.D1 and <= Key.D3 when IsCw && e.KeyModifiers.HasFlag(KeyModifiers.Shift):
|
||||
case >= Key.NumPad1 and <= Key.NumPad3 when IsCw && e.KeyModifiers.HasFlag(KeyModifiers.Shift):
|
||||
e.Handled = true;
|
||||
AskFieldAgain(FieldOf(e.Key));
|
||||
break;
|
||||
case Key.Enter when e.Source is TextBox:
|
||||
e.Handled = true;
|
||||
@@ -286,20 +311,29 @@ public sealed partial class QtcWindow : Window
|
||||
CloseButton.Focus();
|
||||
}
|
||||
|
||||
/// N1MM's RX Ready tells the other station to go ahead, and does it with a
|
||||
/// recording. There is no voice keyer here, so it only puts the cursor
|
||||
/// where the first line will land.
|
||||
private void OnReady(object? sender, RoutedEventArgs e) => rows[0].Time.Focus();
|
||||
|
||||
/// The header of the series we are about to read out: `QTC 3/10`.
|
||||
private void OnHeaderSend(object? sender, RoutedEventArgs e)
|
||||
/// Reading traffic out, this asks the other station whether it is ready;
|
||||
/// taking it down, it says we are. N1MM keys `QRV?` and `QRV` and plays a
|
||||
/// recording on SSB, which there is no keyer for here.
|
||||
private void OnReady(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = send(HeaderBox.Text ?? "");
|
||||
rows[0].Again.Focus();
|
||||
if (IsCw)
|
||||
{
|
||||
_ = send(isSending ? QtcMessages.AreYouReady : QtcMessages.ReadyToReceive);
|
||||
}
|
||||
Control next = isSending ? rows[0].Again : rows[0].Time;
|
||||
next.Focus();
|
||||
}
|
||||
|
||||
/// One button, as N1MM has it: reading traffic out it keys the header of
|
||||
/// the series, taking it down it asks for the header again.
|
||||
private void OnHeaderAgain(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (isSending)
|
||||
{
|
||||
_ = send(HeaderBox.Text ?? "");
|
||||
rows[0].Again.Focus();
|
||||
return;
|
||||
}
|
||||
if (IsCw)
|
||||
{
|
||||
_ = send(QtcMessages.HeaderAgain);
|
||||
@@ -340,6 +374,7 @@ public sealed partial class QtcWindow : Window
|
||||
/// line has gone the cursor lands on Close, which sends the TU message.
|
||||
private void SendLine(int at)
|
||||
{
|
||||
lastSent = at;
|
||||
_ = send(QtcMessages.Line(
|
||||
rows[at].Time.Text ?? "",
|
||||
rows[at].Call.Text ?? "",
|
||||
@@ -356,24 +391,58 @@ public sealed partial class QtcWindow : Window
|
||||
CloseButton.Focus();
|
||||
}
|
||||
|
||||
/// N1MM's Shift+Enter: ask for one field of the line being taken down
|
||||
/// again, and clear it. One message per column, all three in the settings.
|
||||
private void SendFieldAgain(TextBox box)
|
||||
/// 1, 2, 3 and 4 as N1MM numbers them: the time, the call, the serial
|
||||
/// number and the header.
|
||||
private static int FieldOf(Key key) =>
|
||||
key is >= Key.NumPad1 and <= Key.NumPad4 ? key - Key.NumPad1 : key - Key.D1;
|
||||
|
||||
/// Reading traffic out, the number keys send one field of the line last
|
||||
/// sent again, for a station that missed a piece of it.
|
||||
private void SendField(int field)
|
||||
{
|
||||
int column = rows.Select(r => r.Time).Contains(box) ? 0
|
||||
: rows.Select(r => r.Call).Contains(box) ? 1
|
||||
: 2;
|
||||
_ = send(column switch
|
||||
if (field == 3)
|
||||
{
|
||||
_ = send(HeaderBox.Text ?? "");
|
||||
return;
|
||||
}
|
||||
Row row = rows[Math.Max(0, lastSent)];
|
||||
_ = send((field switch
|
||||
{
|
||||
0 => row.Time.Text,
|
||||
1 => row.Call.Text,
|
||||
_ => row.Number.Text,
|
||||
} ?? "").Trim());
|
||||
}
|
||||
|
||||
/// Taking traffic down, shift and a number ask for that field again and
|
||||
/// clear it. One message per field, all three in the settings.
|
||||
private void AskFieldAgain(int field)
|
||||
{
|
||||
_ = send(field switch
|
||||
{
|
||||
0 => session.Settings.QtcCwTimeAgain,
|
||||
1 => session.Settings.QtcCwCallAgain,
|
||||
_ => session.Settings.QtcCwNumberAgain,
|
||||
});
|
||||
box.Text = "";
|
||||
box.Focus();
|
||||
Paint();
|
||||
if (FocusedRow() is { } row)
|
||||
{
|
||||
TextBox box = field switch
|
||||
{
|
||||
0 => row.Time,
|
||||
1 => row.Call,
|
||||
_ => row.Number,
|
||||
};
|
||||
box.Text = "";
|
||||
box.Focus();
|
||||
Paint();
|
||||
}
|
||||
}
|
||||
|
||||
/// The line the cursor is in, or the first one when it is somewhere else.
|
||||
private Row? FocusedRow() =>
|
||||
rows.FirstOrDefault(r => r.Time.IsFocused || r.Call.IsFocused || r.Number.IsFocused)
|
||||
?? rows.FirstOrDefault();
|
||||
|
||||
private void OnLineConfirm(int at)
|
||||
{
|
||||
Control next = at + 1 < rows.Count ? rows[at + 1].Time : CloseButton;
|
||||
@@ -408,9 +477,9 @@ public sealed partial class QtcWindow : Window
|
||||
}
|
||||
}
|
||||
Save(lines);
|
||||
if (isSending && IsCw && session.Settings.QtcCwTu.Trim().Length > 0)
|
||||
if (IsCw && session.Settings.QtcCwTu.Trim().Length > 0)
|
||||
{
|
||||
_ = send(session.Settings.QtcCwTu);
|
||||
_ = send(QtcMessages.Tu(session.Settings.QtcCwTu, HeaderBox.Text ?? ""));
|
||||
}
|
||||
Close(true);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user