From eb31fa5ae1b60d4003371206e717172e7852927a Mon Sep 17 00:00:00 2001 From: ericek111 Date: Thu, 27 Aug 2026 15:32:32 +0000 Subject: [PATCH] Edit and delete contacts in the log Double-click a cell in the log window to change it. The columns follow the contest exchange instead of being fixed in the XAML, so CQ WW gets a Zone column and Sweepstakes gets Nr, Prec, Ck and Sec. QsoEditor holds the column list and applies one field edit. Validation comes from the ExchangeFieldKind the contest declared: a CQ zone is 1 to 40, an ITU zone 1 to 90, a section is looked up in the ARRL list, a grid must parse. A refused edit returns the reason and leaves the log alone, so the cell reverts. Changing the callsign runs the country lookup again. Delete, or the right-click menu, removes the selected contact after a confirmation. Either way the log is rescored, so a multiplier the removed contact held passes to the next contact that claims it. Both go out to the other stations in N1MM's own messages: contactreplace carries oldcall and oldtimestamp, contactdelete names the contact. An incoming edit or delete is matched by contact id first, falling back to call plus timestamp because N1MM does not know our ids. Country, continent and the two prefixes were filled in twice, once when logging and once when editing. They now come from CountryFields. Co-Authored-By: Claude Opus 5 --- .gitignore | 1 + README.md | 29 ++- src/Nonemm.App/AppSession.cs | 54 ++++- src/Nonemm.App/Dialogs/ConfirmDialog.axaml | 14 ++ src/Nonemm.App/Dialogs/ConfirmDialog.axaml.cs | 20 ++ src/Nonemm.App/Windows/LogRow.cs | 58 ++--- src/Nonemm.App/Windows/LogWindow.axaml | 23 +- src/Nonemm.App/Windows/LogWindow.axaml.cs | 114 ++++++++- src/Nonemm.Core/Callsign.cs | 12 + src/Nonemm.Network/ContactMessage.cs | 100 ++++++-- src/Nonemm.Network/ContactUpdate.cs | 23 ++ src/Nonemm.Network/StationNetwork.cs | 33 ++- src/Nonemm.Session/CountryFields.cs | 22 ++ src/Nonemm.Session/LoggingSession.cs | 36 ++- src/Nonemm.Session/QsoChange.cs | 7 + src/Nonemm.Session/QsoColumn.cs | 5 + src/Nonemm.Session/QsoEdit.cs | 13 + src/Nonemm.Session/QsoEditor.cs | 227 ++++++++++++++++++ src/Nonemm.Session/QsoField.cs | 27 +++ tests/Nonemm.Core.Tests/CallsignTests.cs | 17 ++ .../ContactMessageTests.cs | 35 ++- .../LoggingSessionTests.cs | 60 +++++ tests/Nonemm.Session.Tests/QsoEditorTests.cs | 135 +++++++++++ 23 files changed, 953 insertions(+), 112 deletions(-) create mode 100644 src/Nonemm.App/Dialogs/ConfirmDialog.axaml create mode 100644 src/Nonemm.App/Dialogs/ConfirmDialog.axaml.cs create mode 100644 src/Nonemm.Network/ContactUpdate.cs create mode 100644 src/Nonemm.Session/CountryFields.cs create mode 100644 src/Nonemm.Session/QsoChange.cs create mode 100644 src/Nonemm.Session/QsoColumn.cs create mode 100644 src/Nonemm.Session/QsoEdit.cs create mode 100644 src/Nonemm.Session/QsoEditor.cs create mode 100644 src/Nonemm.Session/QsoField.cs create mode 100644 tests/Nonemm.Session.Tests/QsoEditorTests.cs diff --git a/.gitignore b/.gitignore index 930e9f1..65f9c94 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ bin/ obj/ *.user /N1MM/ +/N1MMsln/ diff --git a/README.md b/README.md index 5f4cbf3..f0cc537 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ scorer: red for a dupe, green for a new multiplier, blue for points. | Log | N1MM `.s3db`, Cabrillo 3.0 out, ADIF in and out | | While typing | dupe check, multiplier check, points, country and zone from the country file | | Windows | entry, log, check, bandmap, score summary, packet | +| Editing | double-click a cell in the log to change it, Delete to remove the contact; both go out to the other stations | | Radio | hamlib `rigctld`, reconnecting on its own | | Cluster | DX cluster over telnet, spots feeding the bandmap | | Network | contacts shared with the other stations of a multi-operator entry, in N1MM's own contact message | @@ -94,13 +95,30 @@ messages for CW and for phone. The macros are N1MM's: `{MYCALL}`, `{CALL}`, `{EXCH}`, `{SENTRST}`, `{SENTNR}`, `#` for the serial number, and `{SENTRSTCUT}` for cut numbers. Escape stops sending. +### Editing the log + +Double-click a cell in the log window to change it. The columns follow the +contest exchange, so CQ WW shows a Zone column and Sweepstakes shows Nr, Prec, +Ck and Sec. A value the contest does not accept — zone 41, an ARRL section that +does not exist, a callsign with a space in it — is refused and the reason +appears under the log. Changing the callsign looks the country up again. + +Delete, or the right-click menu, removes the selected contact after a +confirmation. Either way the whole log is scored again, so a multiplier the +removed contact was holding passes to the next contact that claims it. + ### Networked stations **Config → Network** names this station and lists the others. Each contact is sent to them as it is logged, in N1MM's `contactinfo` message, so an N1MM -station on the same network sees them too. With no addresses listed the contacts -are broadcast. A contact that arrives is scored again here from the rules rather -than trusted. +station on the same network sees them too. Editing a contact sends +`contactreplace` and deleting one sends `contactdelete`, the same as N1MM. With +no addresses listed the messages are broadcast. A contact that arrives is scored +again here from the rules rather than trusted. + +An incoming edit or delete is matched by contact id first. N1MM does not know +our ids, so the call and the timestamp are the fallback — that is the pair N1MM +itself keys a contact on. ## Layout @@ -125,8 +143,9 @@ covered by plain unit tests. Working: logging a contest end to end, live dupe and multiplier checking, eight built-in contests plus user-defined ones, Cabrillo and ADIF export, ADIF import, -the log, check, bandmap, score and packet windows, radio control, DX cluster -spots, contacts shared between networked stations, and CW keying. +the log, check, bandmap, score and packet windows, editing and deleting logged +contacts, radio control, DX cluster spots, contacts shared between networked +stations, and CW keying. Checked against N1MM 1.0.11031: a log this program wrote opens in N1MM, which reads the contest, its categories and the contacts. See diff --git a/src/Nonemm.App/AppSession.cs b/src/Nonemm.App/AppSession.cs index a1b9b7d..0f4ba1f 100644 --- a/src/Nonemm.App/AppSession.cs +++ b/src/Nonemm.App/AppSession.cs @@ -112,6 +112,9 @@ public sealed class AppSession : IDisposable Logging.Logged += (_, qso) => Bandmap.Add(new Spot( qso.Call, qso.Frequency, qso.TimestampUtc, SpotSource.Log)); Logging.Logged += (_, qso) => _ = network?.SendAsync(qso, Settings.Station.Callsign); + Logging.Edited += (_, change) => _ = network?.SendEditAsync( + change.Qso, Settings.Station.Callsign, change.OldCall, change.OldTimestampUtc); + Logging.Deleted += (_, qso) => _ = network?.SendDeleteAsync(qso, Settings.Station.Callsign); Check = new CheckWindowSources(Logging, Calls, Bandmap); Save(Settings with { ContestNumber = contestNumber }); ContestChanged?.Invoke(this, EventArgs.Empty); @@ -132,29 +135,60 @@ public sealed class AppSession : IDisposable Settings.NetworkPort, Settings.NetworkStationName.Length > 0 ? Settings.NetworkStationName : Environment.MachineName, Settings.NetworkPeers); - network.ContactArrived += (_, qso) => TakeFromNetwork(qso); + network.UpdateArrived += (_, update) => TakeFromNetwork(update); network.Start(); Changed?.Invoke(this, EventArgs.Empty); } - /// A contact another station logged. It goes into the same log under the - /// contest that is open, and is scored here rather than trusting the - /// points the sender put in the message. - private void TakeFromNetwork(Qso qso) + /// What another station did to its log, applied to ours. The store is + /// written directly rather than through `Logging`, so the change is not + /// broadcast back out again. Points and multipliers are worked out here + /// from the rules instead of trusting what the sender put in the message. + private void TakeFromNetwork(ContactUpdate update) { if (Logging is null || store is null) { return; } - if (Logging.Log.Qsos.Any(q => q.Id == qso.Id)) + int contestNumber = Logging.Instance.ContestNumber; + switch (update) { - return; + case ContactLogged logged when !Logging.Log.Qsos.Any(q => q.Id == logged.Qso.Id): + store.Add(logged.Qso with { ContestNumber = contestNumber, IsOriginal = false }); + break; + case ContactReplaced replaced: + if (FindLocal(replaced.Qso.Id, replaced.OldCall, replaced.OldTimestampUtc) is not { } old) + { + return; + } + store.Update(replaced.Qso with + { + Id = old.Id, + ContestNumber = contestNumber, + IsOriginal = false, + }); + break; + case ContactDeleted deleted: + if (FindLocal(deleted.Id, deleted.Call, deleted.TimestampUtc) is not { } gone) + { + return; + } + store.Delete(gone.Id); + break; + default: + return; } - Qso mine = qso with { ContestNumber = Logging.Instance.ContestNumber, IsOriginal = false }; - store.Add(mine); - OpenContest(Logging.Instance.ContestNumber); + OpenContest(contestNumber); } + /// N1MM keys a contact on its call and time, so a message from N1MM carries + /// no id we would recognise. Fall back to that pair when the id misses. + private Qso? FindLocal(string id, string call, DateTime timestampUtc) => + Logging?.Log.Qsos.FirstOrDefault(q => id.Length > 0 && q.Id == id) + ?? Logging?.Log.Qsos.FirstOrDefault(q => + string.Equals(q.Call.Text, call, StringComparison.OrdinalIgnoreCase) + && q.TimestampUtc == timestampUtc); + /// Starts, restarts or stops the keyer, following what the settings say. /// A keyer that will not open is reported; the program keeps running /// without one. diff --git a/src/Nonemm.App/Dialogs/ConfirmDialog.axaml b/src/Nonemm.App/Dialogs/ConfirmDialog.axaml new file mode 100644 index 0000000..18e86b9 --- /dev/null +++ b/src/Nonemm.App/Dialogs/ConfirmDialog.axaml @@ -0,0 +1,14 @@ + + + +