Four changes to the QTC window, three of them N1MM's behaviour and one an addition. Tab, enter and space all move to the next box the operator types in, and shift and tab move back. The walk covers those boxes and nothing else. N1MM handles the three keys in every box itself — `QTCLinesA_KeyDown` and its two siblings, with `PreviewKeyDown` claiming Tab — rather than leaving them to the tab order, so the cursor never lands on a switched-off line or on a button beside one. Tab was going to Avalonia's default walk, which covered every control in the window. The header now says how many lines take traffic. `QTCHeader_LostFocus` reads the count after the last `/` and hands it to `ColorizeCWRXFields`, which enables that many lines and switches off the rest, capped by how many of the station's ten are still free. All ten were live whatever the header said. Nothing already typed is cleared, which N1MM does under `DontErase`. A time typed short is filled in from the line above as the cursor leaves the box: `52` under `1148` reads as `1152`. `QTCLinesA_LostFocus` takes as many characters off the front of the line above as the typed part is missing, and drops a colon whatever the length. `QtcTraffic.FillTime` is that rule. The addition: while the cursor is in a QTC callsign box the check window answers for that call rather than the entry window's, so a call taken down in traffic is checked the same way one being worked is. `CheckWindowSources` takes the call from `Elsewhere` when the QTC window has set it. Checked by driving the window: header `4/5` left five lines live and read "1 of 5", tab walked header to time to call to serial to the next line's time, shift and tab walked back, `52` on the second line became `1152`, and the check window listed W1AW while `W1A` was typed in a QTC line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
115 lines
4.3 KiB
C#
115 lines
4.3 KiB
C#
using Nonemm.Contests;
|
|
using Nonemm.Core;
|
|
using Nonemm.Core.Calls;
|
|
using Nonemm.Spotting;
|
|
|
|
namespace Nonemm.Session;
|
|
|
|
/// Answers what the callsign being typed could be, a column per source.
|
|
public sealed class CheckWindowSources
|
|
{
|
|
private readonly OperatingPosition session;
|
|
private readonly CallDatabase database;
|
|
private readonly Bandmap bandmap;
|
|
|
|
public CheckWindowSources(OperatingPosition session, CallDatabase database, Bandmap bandmap)
|
|
{
|
|
this.session = session;
|
|
this.database = database;
|
|
this.bandmap = bandmap;
|
|
}
|
|
|
|
/// A callsign being typed somewhere other than the entry window. The QTC
|
|
/// window sets it while the cursor is in one of its callsign boxes, so a
|
|
/// call taken down in traffic is checked the same way one being worked is.
|
|
/// Null the rest of the time, and then the entry window's call is read.
|
|
public string? Elsewhere { get; set; }
|
|
|
|
/// The columns are read off what the operator has typed: the callsign box
|
|
/// feeds the call columns, and whichever exchange box has the cursor feeds
|
|
/// the exchange column.
|
|
public IReadOnlyList<CheckColumn> Columns(int limit = 24)
|
|
{
|
|
string query = (Elsewhere ?? session.Entry.Call).Trim().ToUpperInvariant();
|
|
return
|
|
[
|
|
Column(CheckSource.Log, LoggedCalls(), query, limit),
|
|
new CheckColumn(
|
|
CheckSource.Database,
|
|
database.Count,
|
|
query.Length < PartialMatching.ShortestUsefulQuery
|
|
? []
|
|
: database.Matches(query, limit).Select(Judged).ToList()),
|
|
Column(CheckSource.History, HistoryCalls(), query, limit),
|
|
Column(CheckSource.Bandmap, SpottedCalls(), query, limit),
|
|
ExchangeColumn(limit),
|
|
];
|
|
}
|
|
|
|
/// What the exchange box with the cursor in it could hold: the ARRL
|
|
/// sections, or the states and provinces. A serial number or a report has
|
|
/// no list behind it, so the column stays empty.
|
|
private CheckColumn ExchangeColumn(int limit)
|
|
{
|
|
int focus = session.Entry.Focus;
|
|
// a call typed outside the entry window brings no exchange box with it
|
|
if (Elsewhere is not null || focus == 0)
|
|
{
|
|
return new CheckColumn(CheckSource.Exchange, 0, []);
|
|
}
|
|
ExchangeField field = session.Entry.Exchange[focus - 1];
|
|
IReadOnlyList<string> values = ExchangeValues.For(field.Kind);
|
|
string query = session.Entry[focus].Trim().ToUpperInvariant();
|
|
if (query.Length == 0)
|
|
{
|
|
return new CheckColumn(CheckSource.Exchange, values.Count, []);
|
|
}
|
|
List<CheckCandidate> found = [.. values
|
|
.Where(v => v.StartsWith(query, StringComparison.OrdinalIgnoreCase))
|
|
.Take(limit)
|
|
.Select(v => new CheckCandidate(v, PartialMatchQuality.Exact, null))];
|
|
return new CheckColumn(CheckSource.Exchange, values.Count, found);
|
|
}
|
|
|
|
private CheckColumn Column(
|
|
CheckSource source,
|
|
IReadOnlyList<string> calls,
|
|
string query,
|
|
int limit)
|
|
{
|
|
if (query.Length < PartialMatching.ShortestUsefulQuery)
|
|
{
|
|
return new CheckColumn(source, calls.Count, []);
|
|
}
|
|
List<CheckCandidate> found = [];
|
|
foreach (string call in calls)
|
|
{
|
|
PartialMatchQuality? quality = PartialMatching.Judge(query, call);
|
|
if (quality is not null)
|
|
{
|
|
found.Add(Judged(new PartialMatch(call, quality.Value)));
|
|
}
|
|
}
|
|
return new CheckColumn(
|
|
source,
|
|
calls.Count,
|
|
found.OrderBy(c => c.Quality).ThenBy(c => c.Text, StringComparer.Ordinal).Take(limit).ToList());
|
|
}
|
|
|
|
private CheckCandidate Judged(PartialMatch match) =>
|
|
new(match.Call, match.Quality, session.JudgeCall(match.Call));
|
|
|
|
|
|
private IReadOnlyList<string> LoggedCalls() =>
|
|
session.Log.Qsos.Select(q => q.Call.Text).Distinct(StringComparer.Ordinal).ToList();
|
|
|
|
private IReadOnlyList<string> HistoryCalls() => [.. session.Session.History.Calls];
|
|
|
|
private IReadOnlyList<string> SpottedCalls() =>
|
|
bandmap.All()
|
|
.Where(s => s.IsStation)
|
|
.Select(s => s.Call.Text)
|
|
.Distinct(StringComparer.Ordinal)
|
|
.ToList();
|
|
}
|