diff --git a/src/Nonemm.App/Windows/EntryWindow.axaml b/src/Nonemm.App/Windows/EntryWindow.axaml index 33d542a..fa14b7c 100644 --- a/src/Nonemm.App/Windows/EntryWindow.axaml +++ b/src/Nonemm.App/Windows/EntryWindow.axaml @@ -71,6 +71,7 @@ + diff --git a/src/Nonemm.App/Windows/EntryWindow.axaml.cs b/src/Nonemm.App/Windows/EntryWindow.axaml.cs index a09f849..2c1fcc2 100644 --- a/src/Nonemm.App/Windows/EntryWindow.axaml.cs +++ b/src/Nonemm.App/Windows/EntryWindow.axaml.cs @@ -388,6 +388,10 @@ public sealed partial class EntryWindow : Window e.Handled = true; OnWipe(this, new RoutedEventArgs()); break; + case Key.W when e.KeyModifiers.HasFlag(KeyModifiers.Alt): + e.Handled = true; + OnRestoreWiped(this, new RoutedEventArgs()); + break; case Key.L when e.KeyModifiers.HasFlag(KeyModifiers.Control): e.Handled = true; OnShowLog(this, new RoutedEventArgs()); @@ -682,6 +686,26 @@ public sealed partial class EntryWindow : Window Refresh(); } + /// N1MM's Alt+W: the boxes the last wipe cleared are filled in again. With + /// something typed in them there is nowhere to put it back, so the status + /// line says so rather than throwing the typing away. + private void OnRestoreWiped(object? sender, RoutedEventArgs e) + { + if (Logging is null) + { + return; + } + if (!Logging.RestoreWiped()) + { + Status(Logging.Entry.IsEmpty ? "no wiped contact to put back" : "wipe the boxes first"); + return; + } + ResetEsm(); + SyncBoxes(); + boxes[0].Focus(); + Refresh(); + } + private void OnLogIt(object? sender, RoutedEventArgs e) => LogContact(); /// Opens the callsign's page in a browser, as N1MM's QRZ button does. diff --git a/src/Nonemm.Session/EntryFields.cs b/src/Nonemm.Session/EntryFields.cs index a6f3fdd..455a7ae 100644 --- a/src/Nonemm.Session/EntryFields.cs +++ b/src/Nonemm.Session/EntryFields.cs @@ -89,6 +89,12 @@ public sealed class EntryFields private static bool IsFilledIn(ExchangeField field) => field.IsSent || field.Kind == ExchangeFieldKind.Report; + /// True when the operator has typed nothing. The boxes holding what this + /// station sends are filled in by the program, so they do not count. + public bool IsEmpty => + Call.Trim().Length == 0 && + Exchange.Where((f, at) => !f.IsSent && values[at].Trim().Length > 0).Any() == false; + /// True when everything the contest asks for has been typed. public bool IsComplete => Call.Trim().Length > 0 && diff --git a/src/Nonemm.Session/OperatingPosition.cs b/src/Nonemm.Session/OperatingPosition.cs index ac4338e..fca6848 100644 --- a/src/Nonemm.Session/OperatingPosition.cs +++ b/src/Nonemm.Session/OperatingPosition.cs @@ -149,7 +149,7 @@ public sealed class OperatingPosition return false; } Stack.Add(Entry.Call.Trim(), worth.IsNewMultiplier); - Wipe(); + ClearEntry(); return true; } @@ -162,7 +162,7 @@ public sealed class OperatingPosition return false; } string call = Stack.Next(); - Wipe(); + ClearEntry(); Entry.Call = call; Entry.FocusOn(0); Session.NotifyChanged(); @@ -194,7 +194,7 @@ public sealed class OperatingPosition bool isMultiplier = worth.IsNewMultiplier; string next = Stack.Next(); Stack.Add(call, isMultiplier); - Wipe(); + ClearEntry(); Entry.Call = next; Entry.FocusOn(0); Session.NotifyChanged(); @@ -341,7 +341,8 @@ public sealed class OperatingPosition /// log, Ctrl+A forward, Enter writes the changes back and Esc leaves. public Qso? Editing { get; private set; } - private (string Call, string[] Values, string Name, string Comment)? held; + private TypedEntry? held; + private TypedEntry? wiped; /// Steps one contact back or forward and loads it into the boxes. False /// when the log has nothing that way; stepping forward past the newest @@ -364,7 +365,7 @@ public sealed class OperatingPosition LeaveQuickEdit(); return true; } - held ??= (Entry.Call, [.. Enumerable.Range(1, Entry.FieldCount - 1).Select(f => Entry[f])], OtherName, Comment); + held ??= Typed(); Editing = qsos[wanted]; LoadFrom(Editing); Session.NotifyChanged(); @@ -381,13 +382,7 @@ public sealed class OperatingPosition Editing = null; if (held is { } typed) { - Entry.Call = typed.Call; - for (int field = 1; field < Entry.FieldCount; field++) - { - Entry[field] = typed.Values[field - 1]; - } - OtherName = typed.Name; - Comment = typed.Comment; + PutBack(typed); held = null; } Session.NotifyChanged(); @@ -441,7 +436,37 @@ public sealed class OperatingPosition return -1; } + /// Clears the boxes, keeping what was in them for `RestoreWiped`. An entry + /// with nothing typed in it is not worth keeping, so a second wipe does not + /// throw the first one away. public void Wipe() + { + if (!Entry.IsEmpty) + { + wiped = Typed(); + } + ClearEntry(); + } + + /// N1MM's Alt+W: puts back what the last wipe cleared. False when no wipe + /// has anything to put back, or when the boxes have something in them + /// again — N1MM restores into an empty entry only, so nothing typed since + /// the wipe is lost. + public bool RestoreWiped() + { + if (wiped is not { } typed || !Entry.IsEmpty) + { + return false; + } + PutBack(typed); + Entry.FocusOn(0); + Session.NotifyChanged(); + return true; + } + + /// Wipes without keeping anything: the call has gone somewhere else, such + /// as onto the call stack, so there is nothing to put back. + private void ClearEntry() { Editing = null; held = null; @@ -453,6 +478,26 @@ public sealed class OperatingPosition Session.NotifyChanged(); } + private TypedEntry Typed() => new( + Entry.Call, + [.. Enumerable.Range(1, Entry.FieldCount - 1).Select(f => Entry[f])], + OtherName, + Comment, + SentTime); + + private void PutBack(TypedEntry typed) + { + Entry.Call = typed.Call; + // the contest can have changed since, and with it the boxes there are + for (int field = 1; field < Entry.FieldCount && field <= typed.Values.Count; field++) + { + Entry[field] = typed.Values[field - 1]; + } + OtherName = typed.Name; + Comment = typed.Comment; + SentTime = typed.SentTime; + } + /// The name and comment boxes fill the log columns of the same names. A /// contest that exchanges a name has already filled that column, and what /// was exchanged wins. diff --git a/src/Nonemm.Session/TypedEntry.cs b/src/Nonemm.Session/TypedEntry.cs new file mode 100644 index 0000000..3d036c7 --- /dev/null +++ b/src/Nonemm.Session/TypedEntry.cs @@ -0,0 +1,11 @@ +namespace Nonemm.Session; + +/// What is in the entry boxes, kept so it can be put back: by leaving quick +/// edit, or by Alt+W after a wipe. `Values` holds the exchange boxes in order, +/// so `Values[0]` is box 1. +public sealed record TypedEntry( + string Call, + IReadOnlyList Values, + string Name, + string Comment, + string SentTime); diff --git a/tests/Nonemm.Session.Tests/OperatingPositionTests.cs b/tests/Nonemm.Session.Tests/OperatingPositionTests.cs index cc878eb..618f381 100644 --- a/tests/Nonemm.Session.Tests/OperatingPositionTests.cs +++ b/tests/Nonemm.Session.Tests/OperatingPositionTests.cs @@ -648,4 +648,67 @@ public class OperatingPositionTests [Fact] public void EmptyBoxesGiveNoProblem() => Assert.Null(Session().EntryProblem()); + + /// N1MM's Alt+W: a wipe keeps what was in the boxes and Alt+W puts it back. + [Fact] + public void WhatAWipeClearedComesBack() + { + OperatingPosition session = Session(); + Type(session, "JA1XYZ", "25"); + session.Comment = "loud"; + + session.Wipe(); + Assert.Equal("", session.Entry.Call); + + Assert.True(session.RestoreWiped()); + Assert.Equal("JA1XYZ", session.Entry.Call); + Assert.Equal("25", session.Entry.ValueOf(ExchangeSlot.Zone)); + Assert.Equal("loud", session.Comment); + } + + [Fact] + public void ThereIsNothingToPutBackBeforeAWipe() => + Assert.False(Session().RestoreWiped()); + + /// Wiping empty boxes would otherwise throw away what the wipe before it + /// kept. + [Fact] + public void ASecondWipeKeepsTheFirstOne() + { + OperatingPosition session = Session(); + Type(session, "JA1XYZ", "25"); + + session.Wipe(); + session.Wipe(); + + Assert.True(session.RestoreWiped()); + Assert.Equal("JA1XYZ", session.Entry.Call); + } + + /// N1MM puts the boxes back only when they are empty, so typing is never + /// overwritten by a restore. + [Fact] + public void NothingIsPutBackOverSomethingTyped() + { + OperatingPosition session = Session(); + Type(session, "JA1XYZ", "25"); + session.Wipe(); + session.Entry.Call = "DL2XYZ"; + + Assert.False(session.RestoreWiped()); + Assert.Equal("DL2XYZ", session.Entry.Call); + } + + /// A call put on the stack has not gone anywhere, so there is nothing to + /// put back and Alt+W would only make a second copy of it. + [Fact] + public void AStackedCallIsNotWaitingToBePutBack() + { + OperatingPosition session = Session(); + session.IsRunning = true; + Type(session, "JA1XYZ", "25"); + + Assert.True(session.StackAnother()); + Assert.False(session.RestoreWiped()); + } }