Add the edit contact dialog
Ctrl+Y from the entry window, or Enter on a row in the log, opens the whole contact in one dialog. It reaches the fields the log has no column for: QSX frequency, name, QTH, comment, grid, power, radio number, run position, and the country and WPX prefixes. Previous and Next walk the log without closing and offer to save first. The dialog checks values through the same QsoEditor the log columns use, so a zone the contest will not take is refused the same way in both places. QsoEdit now names the field that was refused, so the dialog can put the cursor back in the right box, and QsoEditor.ApplyAll changes several fields at once. Points and the multiplier flags are shown but not editable. They are worked out from the rules every time the log changes, so a typed value would be lost on the next edit. N1MM lets you type them; we do not, rather than offering a box that does nothing. A contact's own country prefix now wins over the country file when scoring. Without this, correcting the country in the dialog would change what the log shows and what Cabrillo says, but not the score. LoggingSession.Update now broadcasts the change like Edit does, so a contact saved from the dialog reaches the other stations. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
29
src/Nonemm.App/Dialogs/EditContactDialog.axaml
Normal file
29
src/Nonemm.App/Dialogs/EditContactDialog.axaml
Normal file
@@ -0,0 +1,29 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Nonemm.App.Dialogs.EditContactDialog"
|
||||
Title="Edit contact" Width="640" SizeToContent="Height"
|
||||
WindowStartupLocation="CenterOwner" CanResize="False">
|
||||
<DockPanel Margin="14">
|
||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="6" Margin="0,12,0,0">
|
||||
<Button Content="◀" Click="OnPrevious" ToolTip.Tip="Previous contact" />
|
||||
<Button Content="▶" Click="OnNext" ToolTip.Tip="Next contact" />
|
||||
<Button Content="Delete" Click="OnDelete" />
|
||||
<Panel Width="120" />
|
||||
<Button Content="Close" Click="OnClose" IsCancel="True" />
|
||||
<Button Content="Update" Click="OnUpdate" IsDefault="True" />
|
||||
</StackPanel>
|
||||
<TextBlock DockPanel.Dock="Bottom" Name="MessageText" Margin="0,10,0,0"
|
||||
FontSize="11" TextWrapping="Wrap" />
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Name="PositionText" FontSize="11" Opacity="0.75" />
|
||||
<Grid Name="ContactFields" ColumnDefinitions="Auto,*,Auto,*" />
|
||||
<TextBlock Text="Contest" FontWeight="Bold" Margin="0,6,0,0" />
|
||||
<Grid Name="ExchangeFields" ColumnDefinitions="Auto,*,Auto,*" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||
<CheckBox Name="RunBox" Content="Ran this one" />
|
||||
<CheckBox Name="ClaimedBox" Content="Counts towards the score" />
|
||||
</StackPanel>
|
||||
<TextBlock Name="DerivedText" FontSize="11" Opacity="0.75" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</Window>
|
||||
255
src/Nonemm.App/Dialogs/EditContactDialog.axaml.cs
Normal file
255
src/Nonemm.App/Dialogs/EditContactDialog.axaml.cs
Normal file
@@ -0,0 +1,255 @@
|
||||
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 LoggingSession logging;
|
||||
private readonly Dictionary<QsoField, TextBox> boxes = [];
|
||||
private int at;
|
||||
|
||||
public EditContactDialog(LoggingSession 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<QsoField> 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<KeyValuePair<QsoField, string>> Changes(Qso qso) =>
|
||||
[.. boxes
|
||||
.Where(pair => (pair.Value.Text ?? "") != QsoEditor.Read(qso, pair.Key))
|
||||
.Select(pair => new KeyValuePair<QsoField, string>(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<bool> KeepChanges()
|
||||
{
|
||||
if (!HasChanges())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
ConfirmDialog dialog = new(
|
||||
"Save contact",
|
||||
$"Save the changes to {Current.Call.Text}?",
|
||||
"Save");
|
||||
return !await dialog.ShowDialog<bool>(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<bool>(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(),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user