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 ContestSession logging; private readonly Dictionary boxes = []; private int at; public EditContactDialog(ContestSession 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(), }; }