Lay the menu bar out the way N1MM lays it out, and fill in the Edit menu
The menu tree comes from N1MM's EntryWindow.Designer.cs: File, Edit, View, Tools, Config, Window, with the items under the names N1MM gives them. The window list moved out of View into Window; the call history file moved from Config into File > Import. Ctrl+W, Ctrl+L and Ctrl+M now work, so the keys printed in the menu are real. Two commands that had no menu item before: the CW and SSB function key definitions, and the QTC setup area. The rest of the Edit menu: - Ctrl+N asks for a note and puts it in the comment of the contact in the boxes, or of the last logged contact when nothing is typed. - Edit Current Contact opens the Edit Contact form over what is typed but not logged. Update hands the values back to the entry boxes. - Ctrl+Q and Ctrl+A load a logged contact into the boxes and paint them pale yellow. Enter writes the change back, Esc puts back what was being typed, and stepping forward past the newest contact leaves quick edit. - Ctrl+U raises the received serial, or a numeric exchange box when the contest has no serial. - Ctrl+F shows the call being typed in the log window, then the next contact with that call. RadioPosition is now OperatingPosition: it is the operator at one radio, not a place on a band. Looked at under Xvfb on a 3775-contact log: quick edit back and forward, Esc, the note dialog, find, and Edit Current Contact putting a zone back into the entry boxes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
330
src/Nonemm.Session/OperatingPosition.cs
Normal file
330
src/Nonemm.Session/OperatingPosition.cs
Normal file
@@ -0,0 +1,330 @@
|
||||
using Nonemm.Contests;
|
||||
using Nonemm.Core;
|
||||
using Nonemm.Core.Calls;
|
||||
using Nonemm.Core.Country;
|
||||
using Nonemm.Storage;
|
||||
|
||||
namespace Nonemm.Session;
|
||||
|
||||
/// One radio and what the operator is typing on it. A single-radio station has
|
||||
/// one of these; an SO2R station has two, sharing one `ContestSession` and so
|
||||
/// one log, one score and one run of serial numbers.
|
||||
public sealed class OperatingPosition
|
||||
{
|
||||
public OperatingPosition(ContestSession session, int radioNumber = 1)
|
||||
{
|
||||
Session = session;
|
||||
RadioNumber = radioNumber;
|
||||
Entry = new EntryFields(session.Contest.ExchangeFieldsFor(session.Me));
|
||||
}
|
||||
|
||||
public ContestSession Session { get; }
|
||||
|
||||
/// 1 or 2. Contacts record which radio made them.
|
||||
public int RadioNumber { get; }
|
||||
|
||||
public EntryFields Entry { get; }
|
||||
|
||||
/// With no radio connected this stays where it was last typed.
|
||||
public Frequency Frequency { get; private set; } = Bands.Band20M.Low;
|
||||
|
||||
/// Where the radio transmits when working split, and zero when it is not.
|
||||
public Frequency TransmitFrequency { get; private set; } = Frequency.Zero;
|
||||
|
||||
public Mode Mode { get; private set; } = Modes.Cw;
|
||||
|
||||
/// True while the operator is calling CQ on this radio rather than
|
||||
/// searching. Each radio runs or searches on its own.
|
||||
public bool IsRunning { get; set; }
|
||||
|
||||
/// The other operator's name and a note about the contact. Neither is part
|
||||
/// of any exchange — N1MM keeps both beside the exchange boxes and writes
|
||||
/// them into the log — so neither counts towards a complete exchange.
|
||||
public string OtherName { get; set; } = "";
|
||||
|
||||
public string Comment { get; set; } = "";
|
||||
|
||||
public Contest Contest => Session.Contest;
|
||||
|
||||
public ContestInstance Instance => Session.Instance;
|
||||
|
||||
public StationInfo Me => Session.Me;
|
||||
|
||||
public ContestLog Log => Session.Log;
|
||||
|
||||
public QsoEditor Editor => Session.Editor;
|
||||
|
||||
public int SentNumber => Session.SentNumber;
|
||||
|
||||
public void Tune(Frequency frequency, Mode? mode = null, Frequency? transmitFrequency = null)
|
||||
{
|
||||
Frequency = frequency;
|
||||
Mode = mode ?? Mode;
|
||||
TransmitFrequency = transmitFrequency ?? TransmitFrequency;
|
||||
Session.NotifyChanged();
|
||||
}
|
||||
|
||||
/// What the log says about what is typed now. Null when there is no call to
|
||||
/// judge yet.
|
||||
public Verdict? Verdict()
|
||||
{
|
||||
string call = Entry.Call.Trim();
|
||||
return call.Length == 0 ? null : Log.Judge(BuildQso(call));
|
||||
}
|
||||
|
||||
public CountryLookup? Country() =>
|
||||
Entry.Call.Trim().Length == 0 ? null : Session.Countries?.Find(Entry.Call.Trim());
|
||||
|
||||
/// Every earlier contact with the call being typed, newest first.
|
||||
public IReadOnlyList<Qso> WorkedBefore() =>
|
||||
Entry.Call.Trim().Length == 0 ? [] : Log.WorkedBefore(Entry.Call.Trim());
|
||||
|
||||
/// The frequency typed into the callsign box, if that is what it holds.
|
||||
public Frequency? PendingQsy() => FrequencyEntry.Parse(Entry.Call);
|
||||
|
||||
/// Logs what is typed and clears the entry. Throws when the entry is not
|
||||
/// complete, so a caller that has not checked cannot lose an exchange.
|
||||
public Qso LogContact()
|
||||
{
|
||||
if (!Entry.IsComplete)
|
||||
{
|
||||
throw new InvalidOperationException($"the exchange for {Entry.Call} is not complete");
|
||||
}
|
||||
Qso stored = Session.Add(Noted(BuildQso(Entry.Call.Trim())));
|
||||
Entry.Clear();
|
||||
OtherName = "";
|
||||
Comment = "";
|
||||
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.
|
||||
public bool FillFromHistory()
|
||||
{
|
||||
CallHistoryEntry? known = Session.History.Find(Entry.Call);
|
||||
if (known is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool filled = false;
|
||||
for (int at = 0; at < Entry.Exchange.Count; at++)
|
||||
{
|
||||
string value = ExchangeSlots.ValueFrom(known, Entry.Exchange[at]);
|
||||
if (value.Length == 0 || Entry[at + 1].Trim().Length > 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Entry[at + 1] = value;
|
||||
filled = true;
|
||||
}
|
||||
if (filled)
|
||||
{
|
||||
Session.NotifyChanged();
|
||||
}
|
||||
return filled;
|
||||
}
|
||||
|
||||
/// Moves to the next box the operator types in. On top of the report boxes
|
||||
/// the entry itself steps over, a contest can say a box is not for this
|
||||
/// station: CQ WW RTTY asks only the US and Canada for a state.
|
||||
public void MoveFocus(bool forward)
|
||||
{
|
||||
CountryLookup? their = Country();
|
||||
for (int step = 0; step < Entry.FieldCount; step++)
|
||||
{
|
||||
if (forward)
|
||||
{
|
||||
Entry.Advance();
|
||||
}
|
||||
else
|
||||
{
|
||||
Entry.Retreat();
|
||||
}
|
||||
if (Entry.Focus == 0 || !Contest.SkipsField(Entry.Exchange[Entry.Focus - 1], their))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The logged contact the boxes are re-editing, or null while a new one is
|
||||
/// being typed. N1MM calls this quick edit: Ctrl+Q steps back through the
|
||||
/// 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;
|
||||
|
||||
/// 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
|
||||
/// contact leaves quick edit instead.
|
||||
public bool QuickEdit(bool forward)
|
||||
{
|
||||
IReadOnlyList<Qso> qsos = Log.Qsos;
|
||||
if (qsos.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
int at = Editing is null ? qsos.Count : IndexOf(Editing.Id);
|
||||
int wanted = forward ? at + 1 : at - 1;
|
||||
if (wanted < 0 || at < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (wanted >= qsos.Count)
|
||||
{
|
||||
LeaveQuickEdit();
|
||||
return true;
|
||||
}
|
||||
held ??= (Entry.Call, [.. Enumerable.Range(1, Entry.FieldCount - 1).Select(f => Entry[f])], OtherName, Comment);
|
||||
Editing = qsos[wanted];
|
||||
LoadFrom(Editing);
|
||||
Session.NotifyChanged();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Puts the boxes back to the contact that was being typed.
|
||||
public void LeaveQuickEdit()
|
||||
{
|
||||
if (Editing is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
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;
|
||||
held = null;
|
||||
}
|
||||
Session.NotifyChanged();
|
||||
}
|
||||
|
||||
/// Writes the boxes back to the contact being quick edited and leaves quick
|
||||
/// edit. Null when no contact is being edited.
|
||||
public Qso? SaveQuickEdit()
|
||||
{
|
||||
if (Editing is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
Qso saved = Noted(ApplyExchange(CountryFields.Apply(
|
||||
Editing with { Call = Callsign.Parse(Entry.Call.Trim()) }, Session.Countries)));
|
||||
Session.Update(saved);
|
||||
LeaveQuickEdit();
|
||||
return saved;
|
||||
}
|
||||
|
||||
/// The contact as it would be logged now, for the dialog that edits the one
|
||||
/// in progress.
|
||||
public Qso InProgress() => Noted(BuildQso(Entry.Call.Trim()));
|
||||
|
||||
/// Fills the boxes from a contact, as quick edit does.
|
||||
public void LoadFrom(Qso qso)
|
||||
{
|
||||
Entry.Call = qso.Call.Text;
|
||||
for (int at = 0; at < Entry.Exchange.Count; at++)
|
||||
{
|
||||
Entry[at + 1] = ExchangeSlots.ValueOf(qso, Entry.Exchange[at].Slot);
|
||||
}
|
||||
OtherName = Entry.Exchange.Any(f => f.Slot == ExchangeSlot.Name) ? "" : qso.Name;
|
||||
Comment = qso.Comment;
|
||||
}
|
||||
|
||||
private int IndexOf(string id)
|
||||
{
|
||||
for (int at = 0; at < Log.Qsos.Count; at++)
|
||||
{
|
||||
if (Log.Qsos[at].Id == id)
|
||||
{
|
||||
return at;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public void Wipe()
|
||||
{
|
||||
Editing = null;
|
||||
held = null;
|
||||
Entry.Clear();
|
||||
OtherName = "";
|
||||
Comment = "";
|
||||
Session.NotifyChanged();
|
||||
}
|
||||
|
||||
/// 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.
|
||||
private Qso Noted(Qso qso) => qso with
|
||||
{
|
||||
Name = qso.Name.Length > 0 ? qso.Name : OtherName.Trim(),
|
||||
Comment = qso.Comment.Length > 0 ? qso.Comment : Comment.Trim(),
|
||||
};
|
||||
|
||||
/// The report an operator sends without thinking about it: 599 on CW and
|
||||
/// digital modes, 59 on phone.
|
||||
public string DefaultReport() => Mode.Category switch
|
||||
{
|
||||
ModeCategory.Cw => "599",
|
||||
ModeCategory.Phone => "59",
|
||||
_ => "599",
|
||||
};
|
||||
|
||||
private Qso BuildQso(string call)
|
||||
{
|
||||
Qso qso = new()
|
||||
{
|
||||
Id = Qso.NewId(),
|
||||
TimestampUtc = DateTime.UtcNow.AddTicks(-(DateTime.UtcNow.Ticks % TimeSpan.TicksPerSecond)),
|
||||
Call = Callsign.Parse(call),
|
||||
Frequency = Frequency,
|
||||
QsxFrequency = TransmitFrequency,
|
||||
Mode = Mode,
|
||||
RadioNumber = RadioNumber,
|
||||
ContestName = Contest.Name,
|
||||
ContestNumber = Instance.ContestNumber,
|
||||
SentReport = DefaultReport(),
|
||||
SentNumber = SentNumber,
|
||||
Operator = Session.Operator.Length > 0 ? Session.Operator : Me.Callsign,
|
||||
IsRunQso = IsRunning,
|
||||
};
|
||||
return ApplyExchange(CountryFields.Apply(qso, Session.Countries));
|
||||
}
|
||||
|
||||
private Qso ApplyExchange(Qso qso)
|
||||
{
|
||||
foreach (ExchangeField field in Entry.Exchange)
|
||||
{
|
||||
qso = ExchangeSlots.Apply(qso, field.Slot, Entry.ValueOf(field.Slot).Trim());
|
||||
}
|
||||
return qso;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user