Add the name, comment and lights from N1MM's entry window
Three more things off the screenshot of N1MM's entry window. A Name box and a Comment box beside the exchange. Neither is part of the exchange, so an empty one does not hold up a contact and ESM does not wait for it; both fill the log columns of those names, and a contest that exchanges a name keeps what was exchanged. Tab now walks the callsign, the exchange, the name and the comment before coming round again, which is what the boxes being there implies. The name is filled from the call history file, and what that file says in its UserText column is written under the entry boxes, where N1MM writes it. Two lights beside the run switch: green while a radio is answering, red while something is going out on the air. The transmit light follows the keyer, so with cwdaemon it goes out when the message has actually finished rather than when the text was handed over. The window is wider to fit the two new boxes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -91,24 +91,27 @@ public sealed partial class EntryWindow
|
||||
{
|
||||
try
|
||||
{
|
||||
sending = true;
|
||||
Refresh();
|
||||
await keyer.SendAsync(text);
|
||||
}
|
||||
catch (InvalidOperationException e)
|
||||
{
|
||||
sending = false;
|
||||
Status(e.Message);
|
||||
return;
|
||||
}
|
||||
if (after.Count > 0)
|
||||
{
|
||||
_ = RunWhenSentAsync(keyer, after);
|
||||
}
|
||||
_ = RunWhenSentAsync(keyer, after);
|
||||
}
|
||||
|
||||
/// Waits for the keyer, turns the transmit light off, and runs whatever
|
||||
/// stood after `{END}`.
|
||||
private async Task RunWhenSentAsync(MessageSender keyer, IReadOnlyList<MessageAction> after)
|
||||
{
|
||||
await WhenSentAsync(keyer);
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
sending = false;
|
||||
foreach (MessageAction action in after)
|
||||
{
|
||||
Run(action);
|
||||
@@ -188,6 +191,7 @@ public sealed partial class EntryWindow
|
||||
Step(-session.Settings.PageStepHertz);
|
||||
break;
|
||||
case MessageCommand.StopSending:
|
||||
sending = false;
|
||||
_ = session.Keyer?.AbortAsync();
|
||||
break;
|
||||
case MessageCommand.Telnet:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Nonemm.App.Windows.EntryWindow"
|
||||
Title="Nonemm"
|
||||
Width="620" SizeToContent="Height"
|
||||
Width="820" SizeToContent="Height"
|
||||
CanResize="True"
|
||||
FontFamily="Inter, sans-serif">
|
||||
<Window.Styles>
|
||||
@@ -113,10 +113,17 @@
|
||||
<TextBlock Name="FrequencyText" FontFamily="monospace" FontSize="20" Text="14025.00" />
|
||||
<TextBlock Grid.Column="1" Name="ModeText" FontFamily="monospace" FontSize="20"
|
||||
Margin="10,0,0,0" Text="CW" />
|
||||
<Border Grid.Column="2" Name="RunBorder" Margin="10,2" Padding="6,1" CornerRadius="3"
|
||||
Background="#22808080">
|
||||
<TextBlock Name="RunText" FontSize="12" Text="S&P" />
|
||||
</Border>
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="4" Margin="10,2,0,2"
|
||||
VerticalAlignment="Center">
|
||||
<Ellipse Name="RadioLight" Width="9" Height="9" Fill="#33808080"
|
||||
VerticalAlignment="Center" />
|
||||
<Ellipse Name="TransmitLight" Width="9" Height="9" Fill="#33808080"
|
||||
VerticalAlignment="Center" />
|
||||
<Border Name="RunBorder" Padding="6,1" CornerRadius="3" Background="#22808080"
|
||||
Margin="4,0,0,0">
|
||||
<TextBlock Name="RunText" FontSize="12" Text="S&P" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<TextBlock Grid.Column="3" Name="ContestText" FontSize="12" Opacity="0.75"
|
||||
VerticalAlignment="Center" Margin="12,0,12,0" Text="no contest"
|
||||
TextTrimming="CharacterEllipsis" ClipToBounds="True" />
|
||||
@@ -134,6 +141,9 @@
|
||||
<TextBlock Name="PathText" FontSize="11" Opacity="0.75" Margin="2,4,0,0" Text=""
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
|
||||
<TextBlock Name="UserTextText" FontSize="11" Margin="2,2,0,0" Text=""
|
||||
Foreground="#2980B9" TextTrimming="CharacterEllipsis" />
|
||||
|
||||
<TextBlock Name="StatusText" FontSize="11" Opacity="0.7" Margin="2,4,0,0" Text="" />
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
|
||||
@@ -20,6 +20,11 @@ public sealed partial class EntryWindow : Window
|
||||
private EntryWindow? secondRadio;
|
||||
private readonly List<TextBox> boxes = [];
|
||||
private readonly List<Button> functionButtons = [];
|
||||
private static readonly IBrush Dark = new SolidColorBrush(Color.FromArgb(0x33, 0x80, 0x80, 0x80));
|
||||
|
||||
private bool sending;
|
||||
private TextBox? nameBox;
|
||||
private TextBox? commentBox;
|
||||
private readonly DispatcherTimer clock = new() { Interval = TimeSpan.FromSeconds(1) };
|
||||
private readonly Dictionary<Type, Window> openWindows = [];
|
||||
private bool updating;
|
||||
@@ -179,10 +184,55 @@ public sealed partial class EntryWindow : Window
|
||||
ExchangeField field = Logging.Entry.Exchange[at];
|
||||
AddBox(field.Label, field.Width, at + 1);
|
||||
}
|
||||
AddNotes();
|
||||
boxes[0].Focus();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
/// N1MM keeps a name and a comment beside the exchange, whatever the
|
||||
/// contest is. Neither is part of the exchange: they fill the log columns
|
||||
/// of the same names, and a contest that exchanges a name already has one.
|
||||
private void AddNotes()
|
||||
{
|
||||
nameBox = Logging!.Entry.Exchange.Any(f => f.Slot == ExchangeSlot.Name)
|
||||
? null
|
||||
: AddNote("Name", 8, EntryGrid.ColumnDefinitions.Count);
|
||||
commentBox = AddNote("Comment", 12, EntryGrid.ColumnDefinitions.Count);
|
||||
}
|
||||
|
||||
private TextBox AddNote(string label, int width, int index)
|
||||
{
|
||||
EntryGrid.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto));
|
||||
TextBlock caption = new() { Text = label };
|
||||
caption.Classes.Add("label");
|
||||
Grid.SetColumn(caption, index);
|
||||
Grid.SetRow(caption, 0);
|
||||
EntryGrid.Children.Add(caption);
|
||||
|
||||
TextBox box = new()
|
||||
{
|
||||
Width = (width * 13) + 20,
|
||||
Margin = new Avalonia.Thickness(0, 0, 6, 0),
|
||||
HorizontalAlignment = HorizontalAlignment.Left,
|
||||
};
|
||||
box.Classes.Add("entry");
|
||||
box.TextChanged += (_, _) => OnNoteChanged();
|
||||
Grid.SetColumn(box, index);
|
||||
Grid.SetRow(box, 1);
|
||||
EntryGrid.Children.Add(box);
|
||||
return box;
|
||||
}
|
||||
|
||||
private void OnNoteChanged()
|
||||
{
|
||||
if (updating || Logging is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Logging.OtherName = nameBox?.Text ?? "";
|
||||
Logging.Comment = commentBox?.Text ?? "";
|
||||
}
|
||||
|
||||
private void AddBox(string label, int width, int index)
|
||||
{
|
||||
EntryGrid.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto));
|
||||
@@ -239,6 +289,7 @@ public sealed partial class EntryWindow : Window
|
||||
break;
|
||||
case Key.Escape:
|
||||
e.Handled = true;
|
||||
sending = false;
|
||||
session.Alternating?.Stop();
|
||||
_ = session.Keyer?.AbortAsync();
|
||||
Logging.Wipe();
|
||||
@@ -260,7 +311,7 @@ public sealed partial class EntryWindow : Window
|
||||
e.Handled = true;
|
||||
session.SwapRadio();
|
||||
break;
|
||||
case Key.Tab when FocusedIsEntryBox():
|
||||
case Key.Tab when FocusedIsEntryBox() || FocusedIsNoteBox():
|
||||
e.Handled = true;
|
||||
MoveFocus(forward: !e.KeyModifiers.HasFlag(KeyModifiers.Shift));
|
||||
break;
|
||||
@@ -343,19 +394,45 @@ public sealed partial class EntryWindow : Window
|
||||
boxes[0].Focus();
|
||||
}
|
||||
|
||||
/// Tab and space walk the callsign box, the exchange boxes, then the name
|
||||
/// and comment boxes, and round again.
|
||||
private void MoveFocus(bool forward)
|
||||
{
|
||||
if (Logging is null || boxes.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
List<TextBox> notes = [.. new[] { nameBox, commentBox }.OfType<TextBox>()];
|
||||
int inNotes = notes.FindIndex(box => box.IsFocused);
|
||||
if (inNotes >= 0)
|
||||
{
|
||||
int next = inNotes + (forward ? 1 : -1);
|
||||
if (next >= 0 && next < notes.Count)
|
||||
{
|
||||
notes[next].Focus();
|
||||
return;
|
||||
}
|
||||
Logging.Entry.FocusOn(forward ? 0 : Logging.Entry.FieldCount - 1);
|
||||
FocusEntryBox();
|
||||
return;
|
||||
}
|
||||
if (forward && notes.Count > 0 && Logging.Entry.Focus == Logging.Entry.FieldCount - 1)
|
||||
{
|
||||
notes[0].Focus();
|
||||
return;
|
||||
}
|
||||
if (forward)
|
||||
{
|
||||
// leaving the callsign box is the moment the call is settled, which
|
||||
// is when the call history can say what the exchange will be
|
||||
if (Logging.Entry.Focus == 0 && Logging.FillFromHistory())
|
||||
if (Logging.Entry.Focus == 0)
|
||||
{
|
||||
SyncBoxes();
|
||||
bool filled = Logging.FillFromHistory();
|
||||
filled |= FillNameFromHistory();
|
||||
if (filled)
|
||||
{
|
||||
SyncBoxes();
|
||||
}
|
||||
}
|
||||
Logging.Entry.Advance();
|
||||
}
|
||||
@@ -363,16 +440,45 @@ public sealed partial class EntryWindow : Window
|
||||
{
|
||||
Logging.Entry.Retreat();
|
||||
}
|
||||
FocusEntryBox();
|
||||
}
|
||||
|
||||
private void FocusEntryBox()
|
||||
{
|
||||
if (Logging is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
boxes[Logging.Entry.Focus].Focus();
|
||||
boxes[Logging.Entry.Focus].CaretIndex = boxes[Logging.Entry.Focus].Text?.Length ?? 0;
|
||||
}
|
||||
|
||||
/// The call history knows the names in a lot of contests, and the name box
|
||||
/// is where N1MM shows it when the contest does not exchange one.
|
||||
private bool FillNameFromHistory()
|
||||
{
|
||||
if (Logging is null || nameBox is null || Logging.OtherName.Trim().Length > 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
string known = Logging.Session.History.Find(Logging.Entry.Call)?.Name ?? "";
|
||||
if (known.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Logging.OtherName = known;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool FocusedIsEntryBox() => boxes.Any(b => b.IsFocused);
|
||||
|
||||
private bool FocusedIsNoteBox() => nameBox?.IsFocused == true || commentBox?.IsFocused == true;
|
||||
|
||||
/// The buttons N1MM puts under its function keys. Each one does what its
|
||||
/// key does, so there is one place deciding what happens.
|
||||
private void OnStopSending(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
sending = false;
|
||||
session.Alternating?.Stop();
|
||||
_ = session.Keyer?.AbortAsync();
|
||||
Status("stopped sending");
|
||||
@@ -435,6 +541,14 @@ public sealed partial class EntryWindow : Window
|
||||
{
|
||||
boxes[at].Text = Logging.Entry[at];
|
||||
}
|
||||
if (nameBox is not null)
|
||||
{
|
||||
nameBox.Text = Logging.OtherName;
|
||||
}
|
||||
if (commentBox is not null)
|
||||
{
|
||||
commentBox.Text = Logging.Comment;
|
||||
}
|
||||
updating = false;
|
||||
}
|
||||
|
||||
@@ -457,6 +571,8 @@ public sealed partial class EntryWindow : Window
|
||||
|
||||
Title = TitleLine();
|
||||
PathText.Text = PathLine();
|
||||
UserTextText.Text = Logging.Session.History.Find(Logging.Entry.Call)?.UserText ?? "";
|
||||
ShowLights();
|
||||
|
||||
Verdict? verdict = Logging.Verdict();
|
||||
VerdictBorder.Background = Verdicts.Colour(verdict);
|
||||
@@ -483,6 +599,15 @@ public sealed partial class EntryWindow : Window
|
||||
$"{Logging.Log.Tally.Points} pts · {mults} · {Logging.Log.TotalScore:N0}{radio}";
|
||||
}
|
||||
|
||||
/// The two lights N1MM has beside the run switch: the left one says a radio
|
||||
/// is answering, the right one that something is going out on the air.
|
||||
private void ShowLights()
|
||||
{
|
||||
bool radio = session.Radios.Any(r => r.Number == radioNumber && r.IsConnected);
|
||||
RadioLight.Fill = radio ? Verdicts.NewMultiplier : Dark;
|
||||
TransmitLight.Fill = sending ? Verdicts.Dupe : Dark;
|
||||
}
|
||||
|
||||
/// What N1MM writes in its title bar: the frequency, the mode, whether the
|
||||
/// frequency came from a radio or was typed, and which radio this window
|
||||
/// is.
|
||||
|
||||
Reference in New Issue
Block a user