diff --git a/README.md b/README.md
index f0cc537..60c5d84 100644
--- a/README.md
+++ b/README.md
@@ -45,7 +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 |
+| Editing | double-click a cell in the log, or open the whole contact with Ctrl+Y; Delete removes it. All of it goes 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 |
@@ -107,6 +107,18 @@ 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.
+**Edit → Edit Last Contact**, Ctrl+Y, or Enter on a row in the log opens the
+whole contact in one dialog. That is where the fields with no log column live:
+QSX frequency, name, QTH, comment, grid, power, radio number, run position, and
+the country and WPX prefixes. ◀ and ▶ walk the log without closing, offering to
+save first. Points and the multiplier flags are shown but cannot be typed in —
+they are worked out from the rules every time the log changes, so anything typed
+there would be overwritten on the next edit.
+
+Correcting the country prefix by hand changes the score. The country file is a
+best guess for calls it has no rule for, so what the contact says now wins over
+what the file says.
+
### Networked stations
**Config → Network** names this station and lists the others. Each contact is
diff --git a/src/Nonemm.App/Dialogs/EditContactDialog.axaml b/src/Nonemm.App/Dialogs/EditContactDialog.axaml
new file mode 100644
index 0000000..d41fdd6
--- /dev/null
+++ b/src/Nonemm.App/Dialogs/EditContactDialog.axaml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Nonemm.App/Dialogs/EditContactDialog.axaml.cs b/src/Nonemm.App/Dialogs/EditContactDialog.axaml.cs
new file mode 100644
index 0000000..1747531
--- /dev/null
+++ b/src/Nonemm.App/Dialogs/EditContactDialog.axaml.cs
@@ -0,0 +1,255 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Layout;
+using Nonemm.Core;
+using Nonemm.Session;
+
+namespace Nonemm.App.Dialogs;
+
+/// One logged contact with every field on it, including the ones the log window
+/// has no column for. Previous and Next walk the log without closing.
+public sealed partial class EditContactDialog : Window
+{
+ private static readonly QsoField[] ContactRow =
+ [
+ QsoField.Time, QsoField.Call,
+ QsoField.Frequency, QsoField.QsxFrequency,
+ QsoField.Mode, QsoField.Operator,
+ QsoField.SentReport, QsoField.ReceivedReport,
+ QsoField.Name, QsoField.Qth,
+ QsoField.RadioNumber, QsoField.RunPosition,
+ ];
+
+ private static readonly QsoField[] ExchangeRow =
+ [
+ QsoField.SentNumber, QsoField.ReceivedNumber,
+ QsoField.Zone, QsoField.Section,
+ QsoField.Precedence, QsoField.Check,
+ QsoField.Exchange1, QsoField.MiscText,
+ QsoField.GridSquare, QsoField.Power,
+ QsoField.CountryPrefix, QsoField.WpxPrefix,
+ ];
+
+ private readonly LoggingSession logging;
+ private readonly Dictionary boxes = [];
+ private int at;
+
+ public EditContactDialog(LoggingSession logging, string contactId)
+ {
+ this.logging = logging;
+ InitializeComponent();
+ BuildFields(ContactFields, ContactRow);
+ BuildFields(ExchangeFields, ExchangeRow);
+ AddWideField(ExchangeFields, QsoField.Comment);
+ at = Math.Max(0, IndexOf(contactId));
+ Display(Current);
+ }
+
+ private Qso Current => logging.Log.Qsos[at];
+
+ private int IndexOf(string id)
+ {
+ for (int index = 0; index < logging.Log.Qsos.Count; index++)
+ {
+ if (logging.Log.Qsos[index].Id == id)
+ {
+ return index;
+ }
+ }
+ return 0;
+ }
+
+ private void BuildFields(Grid grid, IReadOnlyList fields)
+ {
+ for (int index = 0; index < fields.Count; index++)
+ {
+ int row = index / 2;
+ int column = index % 2 == 0 ? 0 : 2;
+ if (column == 0)
+ {
+ grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
+ }
+ grid.Children.Add(LabelFor(fields[index], row, column));
+ grid.Children.Add(BoxFor(fields[index], row, column + 1));
+ }
+ }
+
+ private void AddWideField(Grid grid, QsoField field)
+ {
+ int row = grid.RowDefinitions.Count;
+ grid.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
+ grid.Children.Add(LabelFor(field, row, 0));
+ TextBox box = BoxFor(field, row, 1);
+ Grid.SetColumnSpan(box, 3);
+ }
+
+ private static TextBlock LabelFor(QsoField field, int row, int column)
+ {
+ TextBlock label = new()
+ {
+ Text = Label(field),
+ Margin = new Avalonia.Thickness(0, 0, 8, 4),
+ VerticalAlignment = VerticalAlignment.Center,
+ };
+ Grid.SetRow(label, row);
+ Grid.SetColumn(label, column);
+ return label;
+ }
+
+ private TextBox BoxFor(QsoField field, int row, int column)
+ {
+ TextBox box = new() { Margin = new Avalonia.Thickness(0, 0, 12, 4) };
+ Grid.SetRow(box, row);
+ Grid.SetColumn(box, column);
+ boxes[field] = box;
+ return box;
+ }
+
+ private void Display(Qso qso)
+ {
+ foreach ((QsoField field, TextBox box) in boxes)
+ {
+ box.Text = QsoEditor.Read(qso, field);
+ }
+ RunBox.IsChecked = qso.IsRunQso;
+ ClaimedBox.IsChecked = qso.IsClaimed;
+ PositionText.Text = $"contact {at + 1} of {logging.Log.Qsos.Count}";
+ DerivedText.Text =
+ $"band {qso.Band?.Name ?? "off band"} · continent {Or(qso.Continent)} · " +
+ $"station prefix {Or(qso.StationPrefix)} · {qso.Points} points · " +
+ $"multipliers {Or(Multipliers(qso))}. Points and multipliers are worked out " +
+ "from the rules every time the log changes, so they cannot be typed in here.";
+ MessageText.Text = "";
+ }
+
+ private static string Or(string text) => text.Length > 0 ? text : "none";
+
+ private static string Multipliers(Qso qso) => string.Concat(
+ qso.IsMultiplier1 ? "1" : "",
+ qso.IsMultiplier2 ? "2" : "",
+ qso.IsMultiplier3 ? "3" : "");
+
+ /// The fields whose box no longer matches the contact.
+ private List> Changes(Qso qso) =>
+ [.. boxes
+ .Where(pair => (pair.Value.Text ?? "") != QsoEditor.Read(qso, pair.Key))
+ .Select(pair => new KeyValuePair(pair.Key, pair.Value.Text ?? ""))];
+
+ private bool HasChanges() =>
+ Changes(Current).Count > 0
+ || RunBox.IsChecked != Current.IsRunQso
+ || ClaimedBox.IsChecked != Current.IsClaimed;
+
+ /// True when the contact was saved. A value the contest will not take stops
+ /// the save and puts the cursor back in the box that holds it.
+ private bool Save()
+ {
+ Qso before = Current;
+ QsoEdit edit = logging.Editor.ApplyAll(before, Changes(before));
+ if (edit.Result is null)
+ {
+ MessageText.Text = edit.Error;
+ if (edit.Field is { } field && boxes.TryGetValue(field, out TextBox? box))
+ {
+ box.Focus();
+ }
+ return false;
+ }
+ logging.Update(edit.Result with
+ {
+ IsRunQso = RunBox.IsChecked == true,
+ IsClaimed = ClaimedBox.IsChecked == true,
+ });
+ // editing the time re-sorts the log, so find the contact again
+ at = IndexOf(before.Id);
+ Display(Current);
+ MessageText.Text = $"{Current.Call.Text} updated";
+ return true;
+ }
+
+ private void OnUpdate(object? sender, RoutedEventArgs e) => Save();
+
+ private async void OnPrevious(object? sender, RoutedEventArgs e) => await Move(-1);
+
+ private async void OnNext(object? sender, RoutedEventArgs e) => await Move(1);
+
+ private async Task Move(int by)
+ {
+ int wanted = at + by;
+ if (wanted < 0 || wanted >= logging.Log.Qsos.Count)
+ {
+ MessageText.Text = by < 0 ? "this is the first contact" : "this is the last contact";
+ return;
+ }
+ if (!await KeepChanges())
+ {
+ return;
+ }
+ at = wanted;
+ Display(Current);
+ }
+
+ /// Offers to save before leaving the contact. False means stay where we are.
+ private async Task KeepChanges()
+ {
+ if (!HasChanges())
+ {
+ return true;
+ }
+ ConfirmDialog dialog = new(
+ "Save contact",
+ $"Save the changes to {Current.Call.Text}?",
+ "Save");
+ return !await dialog.ShowDialog(this) || Save();
+ }
+
+ private async void OnDelete(object? sender, RoutedEventArgs e)
+ {
+ Qso qso = Current;
+ ConfirmDialog dialog = new(
+ "Delete contact",
+ $"Delete {qso.Call.Text} at {qso.TimestampUtc:yyyy-MM-dd HH:mm:ss}? " +
+ "The other stations are told to delete it too.",
+ "Delete");
+ if (!await dialog.ShowDialog(this))
+ {
+ return;
+ }
+ logging.Delete(qso.Id);
+ if (logging.Log.Qsos.Count == 0)
+ {
+ Close();
+ return;
+ }
+ at = Math.Min(at, logging.Log.Qsos.Count - 1);
+ Display(Current);
+ MessageText.Text = $"{qso.Call.Text} deleted";
+ }
+
+ private async void OnClose(object? sender, RoutedEventArgs e)
+ {
+ if (await KeepChanges())
+ {
+ Close();
+ }
+ }
+
+ private static string Label(QsoField field) => field switch
+ {
+ QsoField.Time => "Time UTC",
+ QsoField.Frequency => "Frequency kHz",
+ QsoField.QsxFrequency => "QSX kHz",
+ QsoField.SentReport => "Report sent",
+ QsoField.ReceivedReport => "Report received",
+ QsoField.SentNumber => "Number sent",
+ QsoField.ReceivedNumber => "Number received",
+ QsoField.GridSquare => "Grid square",
+ QsoField.MiscText => "Misc",
+ QsoField.CountryPrefix => "Country prefix",
+ QsoField.WpxPrefix => "WPX prefix",
+ QsoField.RadioNumber => "Radio",
+ QsoField.RunPosition => "Run position",
+ QsoField.Qth => "QTH",
+ _ => field.ToString(),
+ };
+}
diff --git a/src/Nonemm.App/Windows/EntryWindow.Menu.cs b/src/Nonemm.App/Windows/EntryWindow.Menu.cs
index 9a3d7ad..07f412c 100644
--- a/src/Nonemm.App/Windows/EntryWindow.Menu.cs
+++ b/src/Nonemm.App/Windows/EntryWindow.Menu.cs
@@ -176,6 +176,16 @@ public sealed partial class EntryWindow
private void OnExit(object? sender, RoutedEventArgs e) => Close();
+ private async void OnEditLastContact(object? sender, RoutedEventArgs e)
+ {
+ if (Logging is null || Logging.Log.Qsos.Count == 0)
+ {
+ Status("there is nothing in the log yet");
+ return;
+ }
+ await new EditContactDialog(Logging, Logging.Log.Qsos[^1].Id).ShowDialog(this);
+ }
+
private void OnShowLog(object? sender, RoutedEventArgs e) => Show(() => new LogWindow(session));
private void OnShowCheck(object? sender, RoutedEventArgs e) => Show(() => new CheckWindow(session, () => Logging?.Entry.Call ?? ""));
diff --git a/src/Nonemm.App/Windows/EntryWindow.axaml b/src/Nonemm.App/Windows/EntryWindow.axaml
index 825e072..6025102 100644
--- a/src/Nonemm.App/Windows/EntryWindow.axaml
+++ b/src/Nonemm.App/Windows/EntryWindow.axaml
@@ -39,6 +39,9 @@
+
diff --git a/src/Nonemm.App/Windows/EntryWindow.axaml.cs b/src/Nonemm.App/Windows/EntryWindow.axaml.cs
index 74e7c62..490cd92 100644
--- a/src/Nonemm.App/Windows/EntryWindow.axaml.cs
+++ b/src/Nonemm.App/Windows/EntryWindow.axaml.cs
@@ -179,6 +179,10 @@ public sealed partial class EntryWindow : Window
e.Handled = true;
MoveFocus(forward: !e.KeyModifiers.HasFlag(KeyModifiers.Shift));
break;
+ case Key.Y when e.KeyModifiers.HasFlag(KeyModifiers.Control):
+ e.Handled = true;
+ OnEditLastContact(this, new RoutedEventArgs());
+ break;
case Key.OemQuestion when e.KeyModifiers.HasFlag(KeyModifiers.Control):
e.Handled = true;
ToggleRun();
diff --git a/src/Nonemm.App/Windows/LogWindow.axaml b/src/Nonemm.App/Windows/LogWindow.axaml
index f37978c..471e40d 100644
--- a/src/Nonemm.App/Windows/LogWindow.axaml
+++ b/src/Nonemm.App/Windows/LogWindow.axaml
@@ -9,6 +9,7 @@
FontSize="12" FontFamily="monospace">
+
diff --git a/src/Nonemm.App/Windows/LogWindow.axaml.cs b/src/Nonemm.App/Windows/LogWindow.axaml.cs
index 71a94ff..b924823 100644
--- a/src/Nonemm.App/Windows/LogWindow.axaml.cs
+++ b/src/Nonemm.App/Windows/LogWindow.axaml.cs
@@ -112,11 +112,30 @@ public sealed partial class LogWindow : RefreshableWindow
private void OnKeyDown(object? sender, KeyEventArgs e)
{
- if (e.Key == Key.Delete && e.Source is not TextBox)
+ if (e.Source is TextBox)
{
- OnDelete(sender, new RoutedEventArgs());
- e.Handled = true;
+ return;
}
+ switch (e.Key)
+ {
+ case Key.Delete:
+ e.Handled = true;
+ OnDelete(sender, new RoutedEventArgs());
+ break;
+ case Key.Enter:
+ e.Handled = true;
+ OnEditContact(sender, new RoutedEventArgs());
+ break;
+ }
+ }
+
+ private async void OnEditContact(object? sender, RoutedEventArgs e)
+ {
+ if (session.Logging is null || Rows.SelectedItem is not LogRow row)
+ {
+ return;
+ }
+ await new EditContactDialog(session.Logging, row.Qso.Id).ShowDialog(this);
}
private async void OnDelete(object? sender, RoutedEventArgs e)
diff --git a/src/Nonemm.Contests/QsoContext.cs b/src/Nonemm.Contests/QsoContext.cs
index 05e48f1..2054223 100644
--- a/src/Nonemm.Contests/QsoContext.cs
+++ b/src/Nonemm.Contests/QsoContext.cs
@@ -11,9 +11,14 @@ public sealed record QsoContext(Qso Qso, CountryLookup? Country, StationInfo Me)
public ModeCategory ModeCategory => Qso.Mode.Category;
- public string Continent => Country?.Continent ?? Qso.Continent;
+ /// What the contact says wins over the country file. The two agree unless
+ /// the operator corrected the country by hand, and then the correction is
+ /// the point.
+ public string Continent =>
+ Qso.Continent.Length > 0 ? Qso.Continent : Country?.Continent ?? "";
- public string CountryPrefix => Country?.Entity.PrimaryPrefix ?? Qso.CountryPrefix;
+ public string CountryPrefix =>
+ Qso.CountryPrefix.Length > 0 ? Qso.CountryPrefix : Country?.Entity.PrimaryPrefix ?? "";
public int CqZone => Country?.CqZone ?? Qso.Zone;
diff --git a/src/Nonemm.Session/LoggingSession.cs b/src/Nonemm.Session/LoggingSession.cs
index f87a0d0..0aa3dbd 100644
--- a/src/Nonemm.Session/LoggingSession.cs
+++ b/src/Nonemm.Session/LoggingSession.cs
@@ -133,26 +133,29 @@ public sealed class LoggingSession
/// Changes one field of a logged contact. A refused edit leaves the log
/// alone and the caller gets the reason back.
- public QsoEdit Edit(string id, QsoField field, string text)
+ public QsoEdit Edit(string id, QsoField field, string text) =>
+ EditWith(id, before => Editor.Apply(before, field, text));
+
+ /// Replaces a logged contact with one the caller has already built and
+ /// checked. The edit contact dialog changes many fields at once and comes
+ /// in this way.
+ public void Update(Qso qso) => EditWith(qso.Id, _ => QsoEdit.Accept(qso));
+
+ private QsoEdit EditWith(string id, Func change)
{
Qso before = Log.Qsos.FirstOrDefault(q => q.Id == id)
?? throw new InvalidOperationException($"no contact with id {id} in the log");
- QsoEdit edit = Editor.Apply(before, field, text);
+ QsoEdit edit = change(before);
if (edit.Result is not null)
{
- Update(edit.Result);
+ store.Update(edit.Result);
+ Log.Replace(edit.Result);
Edited?.Invoke(this, new QsoChange(edit.Result, before.Call.Text, before.TimestampUtc));
+ Changed?.Invoke(this, EventArgs.Empty);
}
return edit;
}
- public void Update(Qso qso)
- {
- store.Update(qso);
- Log.Replace(qso);
- Changed?.Invoke(this, EventArgs.Empty);
- }
-
private int NextSentNumber() =>
Log.Qsos.Count == 0 ? 1 : Log.Qsos.Max(q => q.SentNumber) + 1;
diff --git a/src/Nonemm.Session/QsoEdit.cs b/src/Nonemm.Session/QsoEdit.cs
index edb65ea..d45d5b5 100644
--- a/src/Nonemm.Session/QsoEdit.cs
+++ b/src/Nonemm.Session/QsoEdit.cs
@@ -3,7 +3,9 @@ using Nonemm.Core;
namespace Nonemm.Session;
/// The result of an edit: either the changed contact or an error message.
-public sealed record QsoEdit(Qso? Result, string Error)
+/// `Field` names the field that was refused, so a dialog can put the cursor
+/// back in the right box.
+public sealed record QsoEdit(Qso? Result, string Error, QsoField? Field = null)
{
public static QsoEdit Accept(Qso qso) => new(qso, "");
diff --git a/src/Nonemm.Session/QsoEditor.cs b/src/Nonemm.Session/QsoEditor.cs
index 5d8ec8b..7e9008d 100644
--- a/src/Nonemm.Session/QsoEditor.cs
+++ b/src/Nonemm.Session/QsoEditor.cs
@@ -28,7 +28,8 @@ public sealed class QsoEditor
public static string Read(Qso qso, QsoField field) => field switch
{
QsoField.Time => qso.TimestampUtc.ToString(TimeFormat, CultureInfo.InvariantCulture),
- QsoField.Frequency => qso.Frequency.Kilohertz.ToString("0.0", CultureInfo.InvariantCulture),
+ QsoField.Frequency => Kilohertz(qso.Frequency),
+ QsoField.QsxFrequency => qso.QsxFrequency.Hertz == 0 ? "" : Kilohertz(qso.QsxFrequency),
QsoField.Mode => qso.Mode.Name,
QsoField.Call => qso.Call.Text,
QsoField.SentReport => qso.SentReport,
@@ -47,12 +48,39 @@ public sealed class QsoEditor
QsoField.Power => qso.Power,
QsoField.Comment => qso.Comment,
QsoField.Operator => qso.Operator,
+ QsoField.CountryPrefix => qso.CountryPrefix,
+ QsoField.WpxPrefix => qso.WpxPrefix,
+ QsoField.RadioNumber => Digits(qso.RadioNumber),
+ QsoField.RunPosition => Digits(qso.RunPosition),
_ => "",
};
/// Returns the contact with the field changed, or an error message. An empty
/// value clears the field, except for the call, time, mode and frequency.
public QsoEdit Apply(Qso qso, QsoField field, string text)
+ {
+ QsoEdit edit = Applied(qso, field, text);
+ return edit.IsAccepted ? edit : edit with { Field = field };
+ }
+
+ /// Applies several fields in one go, stopping at the first value the
+ /// contest will not take.
+ public QsoEdit ApplyAll(Qso qso, IEnumerable> values)
+ {
+ Qso changed = qso;
+ foreach ((QsoField field, string text) in values)
+ {
+ QsoEdit edit = Apply(changed, field, text);
+ if (edit.Result is null)
+ {
+ return edit;
+ }
+ changed = edit.Result;
+ }
+ return QsoEdit.Accept(changed);
+ }
+
+ private QsoEdit Applied(Qso qso, QsoField field, string text)
{
string value = Normalize(field, text);
if (KindError(field, value) is { } refused)
@@ -62,7 +90,8 @@ public sealed class QsoEditor
return field switch
{
QsoField.Time => WithTime(qso, value),
- QsoField.Frequency => WithFrequency(qso, value),
+ QsoField.Frequency => WithFrequency(value, f => qso with { Frequency = f }, allowEmpty: false),
+ QsoField.QsxFrequency => WithFrequency(value, f => qso with { QsxFrequency = f }, allowEmpty: true),
QsoField.Mode => WithMode(qso, value),
QsoField.Call => WithCall(qso, value),
QsoField.SentReport => QsoEdit.Accept(qso with { SentReport = value }),
@@ -81,6 +110,10 @@ public sealed class QsoEditor
QsoField.Power => QsoEdit.Accept(qso with { Power = value }),
QsoField.Comment => QsoEdit.Accept(qso with { Comment = value }),
QsoField.Operator => QsoEdit.Accept(qso with { Operator = value }),
+ QsoField.CountryPrefix => QsoEdit.Accept(qso with { CountryPrefix = value }),
+ QsoField.WpxPrefix => QsoEdit.Accept(qso with { WpxPrefix = value }),
+ QsoField.RadioNumber => Count(value, n => qso with { RadioNumber = n }),
+ QsoField.RunPosition => Count(value, n => qso with { RunPosition = n }),
_ => QsoEdit.Refuse($"{field} cannot be edited"),
};
}
@@ -118,14 +151,18 @@ public sealed class QsoEditor
return QsoEdit.Accept(qso with { TimestampUtc = when });
}
- private static QsoEdit WithFrequency(Qso qso, string value)
+ private static QsoEdit WithFrequency(string value, Func set, bool allowEmpty)
{
+ if (value.Length == 0 && allowEmpty)
+ {
+ return QsoEdit.Accept(set(Frequency.Zero));
+ }
if (!double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out double kilohertz)
|| kilohertz <= 0)
{
return QsoEdit.Refuse($"{value} is not a frequency in kilohertz");
}
- return QsoEdit.Accept(qso with { Frequency = Frequency.FromKilohertz(kilohertz) });
+ return QsoEdit.Accept(set(Frequency.FromKilohertz(kilohertz)));
}
private static QsoEdit WithMode(Qso qso, string value)
@@ -178,6 +215,9 @@ public sealed class QsoEditor
&& number >= low
&& number <= high;
+ private static string Kilohertz(Frequency frequency) =>
+ frequency.Kilohertz.ToString("0.0", CultureInfo.InvariantCulture);
+
private static string Digits(int value) => value > 0 ? value.ToString(CultureInfo.InvariantCulture) : "";
private static IReadOnlyList BuildColumns(
diff --git a/src/Nonemm.Session/QsoField.cs b/src/Nonemm.Session/QsoField.cs
index 9b45aba..f750b18 100644
--- a/src/Nonemm.Session/QsoField.cs
+++ b/src/Nonemm.Session/QsoField.cs
@@ -6,6 +6,7 @@ public enum QsoField
{
Time,
Frequency,
+ QsxFrequency,
Mode,
Call,
SentReport,
@@ -24,4 +25,8 @@ public enum QsoField
Power,
Comment,
Operator,
+ CountryPrefix,
+ WpxPrefix,
+ RadioNumber,
+ RunPosition,
}
diff --git a/tests/Nonemm.Session.Tests/LoggingSessionTests.cs b/tests/Nonemm.Session.Tests/LoggingSessionTests.cs
index e351b5e..501e309 100644
--- a/tests/Nonemm.Session.Tests/LoggingSessionTests.cs
+++ b/tests/Nonemm.Session.Tests/LoggingSessionTests.cs
@@ -209,6 +209,21 @@ public class LoggingSessionTests
Assert.Equal("JA2XYZ", change?.Qso.Call.Text);
}
+ /// The country file gets a call wrong now and then. Correcting the country
+ /// by hand has to change the score, or the correction is decoration.
+ [Fact]
+ public void CorrectingTheCountryChangesTheScore()
+ {
+ LoggingSession session = Session();
+ Type(session, "JA1XYZ", "25");
+ Qso logged = session.LogContact();
+ Assert.Equal(3, session.Log.Tally.Points);
+
+ session.Edit(logged.Id, QsoField.CountryPrefix, "DL");
+
+ Assert.Equal(0, session.Log.Tally.Points);
+ }
+
[Fact]
public void ADeleteReportsTheContactThatWent()
{
diff --git a/tests/Nonemm.Session.Tests/QsoEditorTests.cs b/tests/Nonemm.Session.Tests/QsoEditorTests.cs
index 7bc5310..3ba1656 100644
--- a/tests/Nonemm.Session.Tests/QsoEditorTests.cs
+++ b/tests/Nonemm.Session.Tests/QsoEditorTests.cs
@@ -132,4 +132,54 @@ public class QsoEditorTests
[Fact]
public void AnEmptyNumberReadsBackAsAnEmptyColumn() =>
Assert.Equal("", QsoEditor.Read(Contact() with { Zone = 0 }, QsoField.Zone));
+
+ [Fact]
+ public void SeveralFieldsCanBeChangedAtOnce()
+ {
+ QsoEdit edit = CqWw().ApplyAll(Contact(),
+ [
+ new(QsoField.Call, "DL2XYZ"),
+ new(QsoField.Zone, "14"),
+ new(QsoField.Comment, "first QSO"),
+ ]);
+
+ Assert.Equal("DL2XYZ", edit.Result!.Call.Text);
+ Assert.Equal(14, edit.Result.Zone);
+ Assert.Equal("first QSO", edit.Result.Comment);
+ Assert.Equal("DL", edit.Result.CountryPrefix);
+ }
+
+ [Fact]
+ public void TheFirstValueTheContestWillNotTakeStopsTheWholeChange()
+ {
+ QsoEdit edit = CqWw().ApplyAll(Contact(),
+ [
+ new(QsoField.Call, "DL2XYZ"),
+ new(QsoField.Zone, "41"),
+ ]);
+
+ Assert.Null(edit.Result);
+ Assert.Equal(QsoField.Zone, edit.Field);
+ }
+
+ [Fact]
+ public void ARefusalNamesTheFieldItCameFrom() =>
+ Assert.Equal(QsoField.Call, CqWw().Apply(Contact(), QsoField.Call, "12345").Field);
+
+ [Fact]
+ public void AQsxFrequencyCanBeCleared() =>
+ Assert.Equal(
+ 0,
+ CqWw().Apply(Contact() with { QsxFrequency = Frequency.FromKilohertz(14_200) },
+ QsoField.QsxFrequency, "").Result!.QsxFrequency.Hertz);
+
+ /// The working frequency is what the contact was made on, so it cannot be
+ /// emptied the way the QSX frequency can.
+ [Fact]
+ public void TheWorkingFrequencyCannotBeCleared() =>
+ Assert.False(CqWw().Apply(Contact(), QsoField.Frequency, "").IsAccepted);
+
+ [Fact]
+ public void ACorrectedCountryPrefixIsKept() =>
+ Assert.Equal("DL", CqWw().Apply(Contact(), QsoField.CountryPrefix, "dl").Result!.CountryPrefix);
}