diff --git a/src/Nonemm.App/Dialogs/ChangeOperatorDialog.axaml b/src/Nonemm.App/Dialogs/ChangeOperatorDialog.axaml
new file mode 100644
index 0000000..346f787
--- /dev/null
+++ b/src/Nonemm.App/Dialogs/ChangeOperatorDialog.axaml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Nonemm.App/Dialogs/ChangeOperatorDialog.axaml.cs b/src/Nonemm.App/Dialogs/ChangeOperatorDialog.axaml.cs
new file mode 100644
index 0000000..60d955b
--- /dev/null
+++ b/src/Nonemm.App/Dialogs/ChangeOperatorDialog.axaml.cs
@@ -0,0 +1,49 @@
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+
+namespace Nonemm.App.Dialogs;
+
+/// Which callsign to write against the contacts picked in the log. It closes
+/// with the callsign, or with null when nothing is to change. The operators
+/// already known to the program are in the drop-down; any other callsign is
+/// typed.
+public sealed partial class ChangeOperatorDialog : Window
+{
+ public ChangeOperatorDialog(int contacts, string callsign, IReadOnlyList known)
+ {
+ InitializeComponent();
+ AboutText.Text = contacts == 1
+ ? "Operator callsign for the contact picked in the log"
+ : $"Operator callsign for the {contacts} contacts picked in the log";
+ CallBox.Text = callsign;
+ KnownBox.ItemsSource = known;
+ // the callsign is all this dialog asks for, so the cursor starts in it
+ // and the operator types over what is there
+ Opened += (_, _) =>
+ {
+ CallBox.Focus();
+ CallBox.SelectAll();
+ };
+ }
+
+ private void OnPickedKnown(object? sender, SelectionChangedEventArgs e)
+ {
+ if (KnownBox.SelectedItem is string callsign)
+ {
+ CallBox.Text = callsign;
+ }
+ }
+
+ /// An empty box leaves the dialog open: clearing the operator of a contact
+ /// is not what this dialog is for.
+ private void OnOk(object? sender, RoutedEventArgs e)
+ {
+ string callsign = (CallBox.Text ?? "").Trim().ToUpperInvariant();
+ if (callsign.Length > 0)
+ {
+ Close(callsign);
+ }
+ }
+
+ private void OnCancel(object? sender, RoutedEventArgs e) => Close(null);
+}