diff --git a/README.md b/README.md index f748a35..93c4253 100644 --- a/README.md +++ b/README.md @@ -177,10 +177,20 @@ to `SM3ABC`, and the key sends `SM3ABC TU DL1ABC`. Beside the exchange are a Name box and a Comment box, as N1MM has them. Neither is part of the exchange, so neither holds up a contact: they fill the log's Name -and Comment columns, and a contest that exchanges a name uses that instead. Tab -walks the callsign, the exchange, the name and the comment, and round again. The -name is filled from the call history file when there is one, and what the call -history says in its UserText column appears under the entry boxes. +and Comment columns, and a contest that exchanges a name uses that instead. +Neither is in the space and tab walk, as neither is in N1MM's — they are there +for the mouse. The name is filled from the call history file when there is one, +and what the call history says in its UserText column appears under the entry +boxes. + +### Space and tab + +Space and tab move between the callsign box and the exchange boxes and round +again, which is N1MM's walk. The report boxes are stepped over, because nobody +types 599: leaving the callsign box fills them with the report for the mode, +599 on CW and digital, 59 on phone. So in CQ WW, space takes you from the +callsign straight to the zone, and space again brings you back to the callsign. +Click a report box to change one. Two lights sit beside the run switch: the left one is green while a radio is answering, the right one red while something is going out on the air. diff --git a/src/Nonemm.App/Windows/EntryWindow.axaml.cs b/src/Nonemm.App/Windows/EntryWindow.axaml.cs index 351f1a6..12c09ce 100644 --- a/src/Nonemm.App/Windows/EntryWindow.axaml.cs +++ b/src/Nonemm.App/Windows/EntryWindow.axaml.cs @@ -311,7 +311,7 @@ public sealed partial class EntryWindow : Window e.Handled = true; session.SwapRadio(); break; - case Key.Tab when FocusedIsEntryBox() || FocusedIsNoteBox(): + case Key.Tab when FocusedIsEntryBox(): e.Handled = true; MoveFocus(forward: !e.KeyModifiers.HasFlag(KeyModifiers.Shift)); break; @@ -394,40 +394,24 @@ 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. + /// Tab and space walk the callsign box and the exchange boxes, and round + /// again, as N1MM's do. The name and comment boxes are not in the walk, + /// because they are not part of the exchange; they are there for the mouse. private void MoveFocus(bool forward) { if (Logging is null || boxes.Count == 0) { return; } - List notes = [.. new[] { nameBox, commentBox }.OfType()]; - 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 + // is when the call history can say what the exchange will be and + // the reports can be filled in if (Logging.Entry.Focus == 0) { - bool filled = Logging.FillFromHistory(); + bool filled = Logging.FillReports(); + filled |= Logging.FillFromHistory(); filled |= FillNameFromHistory(); if (filled) { @@ -472,8 +456,6 @@ public sealed partial class EntryWindow : Window 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) diff --git a/src/Nonemm.Session/EntryFields.cs b/src/Nonemm.Session/EntryFields.cs index 9c37805..79fbb39 100644 --- a/src/Nonemm.Session/EntryFields.cs +++ b/src/Nonemm.Session/EntryFields.cs @@ -64,11 +64,27 @@ public sealed class EntryFields public void FocusOn(int field) => Focus = Math.Clamp(field, 0, FieldCount - 1); - /// What space does: move to the next box, and round to the callsign again - /// from the last one. - public void Advance() => Focus = (Focus + 1) % FieldCount; + /// What space and tab do: move to the next box the operator types in, and + /// round to the callsign again from the last one. The report boxes are + /// stepped over, because nobody types 599 — N1MM steps over them too, and + /// fills them in instead. + public void Advance() => Focus = Next(1); - public void Retreat() => Focus = (Focus + FieldCount - 1) % FieldCount; + public void Retreat() => Focus = Next(-1); + + private int Next(int by) + { + int at = Focus; + for (int step = 0; step < FieldCount; step++) + { + at = (at + by + FieldCount) % FieldCount; + if (at == 0 || Exchange[at - 1].Kind != ExchangeFieldKind.Report) + { + return at; + } + } + return Focus; + } /// True when everything the contest asks for has been typed. public bool IsComplete => diff --git a/src/Nonemm.Session/RadioPosition.cs b/src/Nonemm.Session/RadioPosition.cs index 41894f1..25d2fe0 100644 --- a/src/Nonemm.Session/RadioPosition.cs +++ b/src/Nonemm.Session/RadioPosition.cs @@ -97,6 +97,28 @@ public sealed class RadioPosition return stored; } + /// Puts the report the operator would send anyway into the report boxes + /// that are empty: 599 on CW and digital modes, 59 on phone. N1MM does the + /// same when the callsign box is left, which is why neither program stops + /// on a report box. + public bool FillReports() + { + bool filled = false; + for (int at = 0; at < Entry.Exchange.Count; at++) + { + if (Entry.Exchange[at].Kind == ExchangeFieldKind.Report && Entry[at + 1].Trim().Length == 0) + { + Entry[at + 1] = DefaultReport(); + filled = true; + } + } + if (filled) + { + Session.NotifyChanged(); + } + return filled; + } + /// Fills what the call history knows into the exchange boxes that are still /// empty. What the other station actually sends wins, so a box with /// something in it is left alone. Returns true when anything was filled. diff --git a/tests/Nonemm.Session.Tests/RadioPositionTests.cs b/tests/Nonemm.Session.Tests/RadioPositionTests.cs index 1881674..9bd565f 100644 --- a/tests/Nonemm.Session.Tests/RadioPositionTests.cs +++ b/tests/Nonemm.Session.Tests/RadioPositionTests.cs @@ -118,15 +118,48 @@ public class RadioPositionTests } [Fact] - public void SpaceMovesThroughTheBoxesAndBackToTheCall() + public void SpaceStepsOverTheReportAndComesBackToTheCall() + { + // CQ WW's boxes are the call, the report and the zone + RadioPosition session = Session(); + + Assert.Equal(0, session.Entry.Focus); + session.Entry.Advance(); + Assert.Equal(2, session.Entry.Focus); + session.Entry.Advance(); + Assert.Equal(0, session.Entry.Focus); + } + + [Fact] + public void GoingBackStepsOverTheReportToo() { RadioPosition session = Session(); + session.Entry.FocusOn(2); + + session.Entry.Retreat(); + Assert.Equal(0, session.Entry.Focus); - session.Entry.Advance(); - Assert.Equal(1, session.Entry.Focus); - session.Entry.Advance(); - session.Entry.Advance(); - Assert.Equal(0, session.Entry.Focus); + } + + [Fact] + public void TheReportBoxesAreFilledInWithWhatWouldBeSentAnyway() + { + RadioPosition session = Session(); + + Assert.True(session.FillReports()); + + Assert.Equal("599", session.Entry.ValueOf(ExchangeSlot.ReceivedReport)); + Assert.False(session.FillReports()); + } + + [Fact] + public void AReportTheOperatorTypedIsLeftAlone() + { + RadioPosition session = Session(); + session.Entry.Set(ExchangeSlot.ReceivedReport, "559"); + + Assert.False(session.FillReports()); + Assert.Equal("559", session.Entry.ValueOf(ExchangeSlot.ReceivedReport)); } [Theory]