Let the operator pick, order and sort the log columns

The log showed the contest's columns and nothing else, with the country,
points and multiplier columns fixed in the XAML. LogColumns now holds every
field the log can show and the settings file keeps the names, left to right.

A right click on a header adds or removes a column and offers the contest's
own set back, a header can be dragged to move its column, and a click sorts
on it. Sorting uses the field, not the text of the cell, so 9,000 kHz comes
before 14,000 and the serial numbers do not run 1, 10, 2. Both grids get the
same columns, so a value in the pane below sits under the column it belongs
to. Removing the last column is refused, otherwise nothing could be selected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
This commit is contained in:
2026-09-10 11:38:30 +00:00
co-authored by Claude Opus 5
parent d5788da240
commit 33c7631e17
7 changed files with 478 additions and 101 deletions
+95
View File
@@ -0,0 +1,95 @@
using Nonemm.Core;
namespace Nonemm.Session;
/// The columns the log window can show, and how a set the operator picked is
/// stored. The contest gives the default set and the labels for its own
/// exchange fields; every other field can be added to it from the header menu.
public static class LogColumns
{
/// Every field the log can show, in the order the header menu lists the
/// ones the contest did not ask for. Labels and widths are the fallback
/// for a field the contest has no column for.
public static readonly IReadOnlyList<QsoColumn> All =
[
new QsoColumn("Time", QsoField.Time, 19),
new QsoColumn("Freq", QsoField.Frequency, 8),
new QsoColumn("QSX", QsoField.QsxFrequency, 8),
new QsoColumn("Mode", QsoField.Mode, 5),
new QsoColumn("Call", QsoField.Call, 12),
new QsoColumn("Snt", QsoField.SentReport, 4),
new QsoColumn("SntNr", QsoField.SentNumber, 5),
new QsoColumn("Rcv", QsoField.ReceivedReport, 4),
new QsoColumn("Nr", QsoField.ReceivedNumber, 5),
new QsoColumn("Zone", QsoField.Zone, 4),
new QsoColumn("Sect", QsoField.Section, 5),
new QsoColumn("Prec", QsoField.Precedence, 4),
new QsoColumn("Ck", QsoField.Check, 3),
new QsoColumn("Exch", QsoField.Exchange1, 6),
new QsoColumn("Misc", QsoField.MiscText, 6),
new QsoColumn("Name", QsoField.Name, 10),
new QsoColumn("QTH", QsoField.Qth, 10),
new QsoColumn("Grid", QsoField.GridSquare, 6),
new QsoColumn("Pwr", QsoField.Power, 5),
new QsoColumn("Comment", QsoField.Comment, 16),
new QsoColumn("Op", QsoField.Operator, 8),
new QsoColumn("Cty", QsoField.CountryPrefix, 6),
new QsoColumn("WPX", QsoField.WpxPrefix, 6),
new QsoColumn("Radio", QsoField.RadioNumber, 5),
new QsoColumn("Run", QsoField.RunPosition, 3),
new QsoColumn("Pts", QsoField.Points, 4, IsEditable: false),
new QsoColumn("Mult", QsoField.Multipliers, 5, IsEditable: false),
];
/// What the header menu offers: the contest's own columns first, in the
/// order the contest gives them, then the rest.
public static IReadOnlyList<QsoColumn> Choices(IReadOnlyList<QsoColumn> contest) =>
[
.. contest,
.. All.Where(c => !contest.Any(other => other.Field == c.Field)),
];
/// The stored field names turned back into columns. A name that is not a
/// field, or is repeated, is dropped. An empty or unusable list falls back
/// to the contest's own columns, which is what a fresh log shows.
public static IReadOnlyList<QsoColumn> Chosen(
IReadOnlyList<QsoColumn> contest,
IEnumerable<string> stored)
{
IReadOnlyList<QsoColumn> offered = Choices(contest);
List<QsoColumn> chosen = [];
foreach (string name in stored)
{
if (!Enum.TryParse(name, out QsoField field)
|| chosen.Any(c => c.Field == field)
|| offered.FirstOrDefault(c => c.Field == field) is not { } column)
{
continue;
}
chosen.Add(column);
}
return chosen.Count > 0 ? chosen : contest;
}
/// What a column sorts on. Numbers and times sort as numbers and times, not
/// as the text the log shows, so 9,000 kHz comes before 14,000 kHz and the
/// serial numbers do not run 1, 10, 2.
public static IComparable SortKey(Qso qso, QsoField field) => field switch
{
QsoField.Time => qso.TimestampUtc,
QsoField.Frequency => qso.Frequency.Hertz,
QsoField.QsxFrequency => qso.QsxFrequency.Hertz,
QsoField.SentNumber => qso.SentNumber,
QsoField.ReceivedNumber => qso.ReceivedNumber,
QsoField.Zone => qso.Zone,
QsoField.Check => qso.Check,
QsoField.RadioNumber => qso.RadioNumber,
QsoField.RunPosition => qso.RunPosition,
QsoField.Points => qso.Points,
_ => QsoEditor.Read(qso, field),
};
/// What goes in the settings file: the field names, left to right.
public static IReadOnlyList<string> Store(IEnumerable<QsoColumn> columns) =>
[.. columns.Select(c => c.Field.ToString())];
}